Files
wpdev 9fa84845be fix: NinjaFirewall 相容性 — 保護 WAF 設定選項不被重導向
評估 NinjaFirewall (WP Edition) 4.9 的相容性,結論是兩者可共存且
不需要開發 AddOn(該外掛全 codebase 零個 apply_filters('nfw_*') /
do_action('nfw_*'),官方相容手段全在部署層)。但核心有一個隱患必須補。

TMDO_Options_Manager::register_settings_group() 以 pre_update_option_{key}
回傳 $old_value,讓選項不再落地 wp_options、改存專屬設定表。而
NinjaFirewall 的 Full WAF 走 auto_prepend_file,在 WordPress 載入前就以
原生 mysqli 直查 wp_options 取 nfw_options / nfw_rules。一旦這些鍵被
重導向,WAF 會讀不到設定而靜默停止防護 —— 不報錯、不寫 log。

nfw_rules 約 77KB 且 autoload=auto,正是 autoload 瘦身最誘人的目標,
因此這條路徑並非理論風險。

Added
- PROTECTED_OPTIONS 常數與註冊守衛(nfw_options / nfw_rules / nfw_checked),
  命中時發出 _doing_it_wrong()。守衛置於方法開頭,全部鍵都被擋時提前返回,
  不再建立空的設定表。
- tests/unit/OptionsManagerProtectedTest.php(4 tests / 10 assertions),
  鎖住「受保護鍵絕不會被掛上 pre_option_* / pre_update_option_* 攔截」。
- docs/WAF-COMPATIBILITY.md:模式差異、symlink 多租戶部署、WP SaaS 開站
  流程與驗證清單、三條開發約束、實測風險矩陣。

Changed
- Migration Wizard 輪詢 500ms → 2s,與四個 stress-test 面板一致。
  原本 2 req/s 打同一 REST endpoint,易觸發 WAF rate-limit 與 bot 偵測。

autoload 最佳化不受影響:optimize_autoload() 只改 autoload 欄位、不刪列,
而 Full WAF 的 SELECT * 不看 autoload。

驗證:591 tests / 1166 assertions 通過,PHPCS 零違規,版本一致性 1.0.2。
dev30 於 Full WAF 與 WP WAF 兩種模式下實測,firewall log 中 TMDO 相關
攔截 0 筆。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdjXAU473eekjPBB8vPVRS
2026-08-15 20:38:09 +08:00

22 KiB
Raw Permalink Blame History

PLAN — 2meet Data Optimizer (核心)

完整四階段拆分計畫見 ~/.claude/plans/wp-data-optimizer-2meet-data-optimizer-zesty-liskov.md


Current state

  • Version: 1.0.2 (2026-08-15;scaffold + 4 phase 完成於 2026-05-15)
  • Phase: 全 4 phase 已完成
  • Source: 從 wp-data-optimizer v2.16.0 提煉

Phase 0 ✅ — 骨架建立 (2026-05-15)

  • 12 plugin 目錄結構(核心 + 11 AddOn)
  • 主檔 2meet-data-optimizer.php + Plugin Header
  • includes/class-tmdo-bootstrap.php stub
  • uninstall.php stub
  • composer.json
  • .gitignore
  • 7 件 MD(README / PLAN / CHANGELOG / DEPLOY / SECURITY / DESIGN / CLAUDE)

Phase 1 ✅ — Core v0.1.0 (2026-05-15)

  • rsync 從 wp-data-optimizer 複製 ~110 個核心檔案,排除 HP/WC/LP/2meet 整合
  • 檔名 class-wpdo-* → class-tmdo-*(含 abstract / interface / trait)
  • sed 內容批次 rename:WPDO_ → TMDO_(class + 常數,hook/table/option 前綴 wpdo_ 保留共用)
  • 常數 TMDO_PLUGIN_DIR → TMDO_PATH 等對齊
  • Text-domain 'wp-data-optimizer' → '2meet-data-optimizer'
  • 主檔重寫成完整 loader(require chain ~150 行)
  • 加 tmdo_run() boot 函式 + plugins_loaded:4 action
  • 加 late-bind safety net (priority 30) 確保 AddOn listener 收到 hook
  • 加 includes/class-tmdo-back-compat.php — 80+ class_alias + 8 常數 alias + 9 hook dual-fire
  • 移除 register_hivepress_defaults() (193 行) → 搬到 HP AddOn
  • 把 core 內所有 HP/WC/LP 整合 class 引用加 class_exists 守門
  • PHP lint:111 files PASS
  • Live test:wp tmdo doctor 全 PASS、wp wpdo (alias) 通

Phase 1 DoD:✅ 全達標(除舊 unit/integration test 套件需後續 phase 1 follow-up)


Phase 2 ✅ — Hub + Spoke AddOn v0.1.0 (2026-05-15)

Hub AddOn (2meet-data-optimizer-hub-addon)

  • TMDO_Hub_Bootstrap 主啟動類別
  • TMDO_Hub_Vendor_Field — 註冊 _2meet_global_vendor_id (hp_vendor / cold zone / indexed)
  • TMDO_Hub_Custom_Tables — 註冊 12 張 2mhc_* 自訂表(fallback 列表 + delegate to hub-core helper if available)
  • dual-emit 支援 (tmdo_* + wpdo_* 兩個 hook 路徑)
  • PHP lint PASS

Spoke AddOn (2meet-data-optimizer-spoke-addon)

  • TMDO_Spoke_Bootstrap 主啟動類別
  • TMDO_Spoke_SSO_Group — 註冊 user entity sso group(7 欄位 → wp_wpdo_user_sso)
  • TMDO_Spoke_Custom_Tables — 註冊 2mso_sso_config / 2mso_user_mapping
  • TMDO_Spoke_CLI::sync() — 從舊 _tmso_* usermeta 遷移
  • TMDO_Spoke_CLI::force_logout() — 設置 token_expires_at = 過去
  • CLI 雙命名空間:wp tmdo spoke-* + wp wpdo spoke-*
  • PHP lint PASS

Phase 2 Live test 結果:

  • ✅ Hub: 12 hub tables 全部 OK
  • ✅ Spoke: SSO group 7 fields registered, 2 tables registered
  • ✅ CLI: wp tmdo spoke-sync --dry-run 跑通,spoke-force-logout --user-id=1 --dry-run 跑通

Phase 3 ✅ — HP + WC + LP AddOn v0.1.0 (2026-05-15)

HivePress AddOn (2meet-data-optimizer-hivepress-addon, ~7,400 行)

  • 複製 13 adapter(core / reviews / bookings / messages / memberships / requests / favorites / statistics / tags / seo / social-links / blocks / marketplace)
  • 複製 7 HPCT interceptor(reviews / messages / favorites / memberships / statistics / requests / listing-meta)
  • 複製 5 query interceptor(reviews / messages / memberships / requests / listing-meta)
  • 複製 補助組件(bootstrap framework / detector / conflict-guard / comment-router / cron-optimizer / attribute-bridge / suitability-scorer / benchmark / rest)
  • 複製 admin tab + CLI namespace
  • 複製 HP transient filter / HPCT import / Listing Stats / term-comment-fields
  • TMDO_HP_Bootstrap outer bootstrap(避免與 inner family bootstrap class 衝突)
  • PHP lint:46 files PASS

WooCommerce AddOn (2meet-data-optimizer-woocommerce-addon, ~1,316 行)

  • 複製 4 個 WC 整合檔
  • TMDO_Woocommerce_Bootstrap 載入 + register
  • PHP lint PASS

LatePoint AddOn (2meet-data-optimizer-latepoint-addon, ~151 行)

  • 複製 LatePoint interceptor
  • TMDO_Latepoint_Bootstrap 載入 + register_hooks
  • PHP lint PASS

Phase 3 Live test 結果:

  • ✅ Plugin 全 active
  • ✅ Hot zone 20 fields registered(mostly WC product/order)
  • ✅ Cold zone 3 fields
  • ✅ Total 23 fields
  • ✅ HP no-op when HivePress not installed(detector check)

Phase 4 ✅ — 6 個 2meet 家族 AddOn v0.1.0 (2026-05-15)

AddOn Source Status
infocards class-tmdo-infocards.php ✅
bookings class-tmdo-bookings.php ✅
quotation class-tmdo-quotation.php ✅
mobile-bridge class-tmdo-mobile-bridge.php ✅
collab class-tmdo-collab.php ✅
playlist class-tmdo-playlist.php ✅

每個 AddOn 都有:

  • TMDO_{Module}_Bootstrap 啟動類別
  • 核心整合 class(從 wp-data-optimizer 搬入)
  • partner detection (early return if target plugin not installed)
  • register() 掛 wpdo_register_fields / wpdo_register_custom_tables

Phase 4 Live test 結果:

  • ✅ 6 AddOn 全 active
  • ✅ Target plugin 未安裝時正確 no-op (designed behavior)
  • ✅ 0 fatal、0 lint error

整體驗收(2026-05-15 末)

指標 結果
12 plugin active ✅
PHP lint 全 files ✅ 203 files PASS
wp tmdo CLI ✅ 18 個 subcommand 全 listed
wp wpdo alias CLI ✅ 通
wp tmdo doctor ✅ system tables 全綠 + 14 partner tables 註冊
wp tmdo status ✅ 23 fields registered
SSO group 7 欄位 ✅ 全 7 欄位(含 hub_global_user_id searchable)
Hub-addon 12 表 ✅ 全 12 表存在 + doctor PASS
Spoke-addon 2 表 ✅ 註冊 OK(DB 表 missing 是 spoke-sso 從未啟用之故)
WC 20 表 ✅ 註冊 OK(DB 表 missing 是 WC 未安裝)
6 family AddOn no-op ✅ target 未安裝時正確 early return

Lessons learned

Phase 1

  • rsync 的 --exclude 對 tests/ 與 tools/ top-level 不生效(因為 source 路徑沒這個前綴),需後續 rm -rf 清理
  • sed 全檔大寫 WPDO_ → TMDO_ 不影響 hook/table/option 小寫前綴 wpdo_,確保資料相容
  • 主檔的 require chain 無法以 sed 自動產出(需手動寫,因為 require 路徑與檔名變動)
  • TMDO_Core::run() 內有 8 處對「整合層 class」的硬引用,需逐個加 class_exists 守門
  • class_alias() 對 trait 與 interface 不適用(需 wrapping wrapper,留 Phase 1 follow-up)
  • WP 6.5+ Requires Plugins: 強制檢查 Plugin Name 而非 slug;hub-core / spoke-sso 仍寫死 Requires Plugins: wp-data-optimizer 故無法直接啟用 → Phase 5 計畫請求 hub-core / spoke-sso 端更新或加 or 2meet-data-optimizer 邏輯

Phase 2

  • inner Hub class TMDO_Hub_Bootstrap 需與 outer AddOn bootstrap 區分名稱(衝突避免)
  • TMDO_Entity_Registry::register_group() 是公開 API;其他 read methods (get_group_fields not get_fields) 命名不直觀

Phase 3

  • HP family 內部已有 TMDO_HivePress_Bootstrap class(v3.0.0 family bootstrap),故 outer AddOn bootstrap 改名 TMDO_HP_Bootstrap 避免衝突

Phase 4

  • 所有 family integration class 皆有 partner detection (e.g. class_exists('TMEETIC_Plugin')) → AddOn 無需自己重複偵測

Phase 5 follow-up 執行狀態(2026-05-15)

# 項目 狀態
1 Tests 套件搬遷:tests/unit/ 與 tests/integration/ 重命名 + path 修正 ✅ 完成
2 PHPCS 套件:補 phpcs.xml.dist + WordPress coding standards ⬜ 待執行
3 PHPUnit bootstrap:完整重建 unit/integration test 環境 ✅ 完成
4 DB version migration:TMDO_DB_VERSION 對齊 SCHEMA_VERSION = '2.0.0' ✅ 完成
5 Requires Plugins: 2meet-data-optimizer — 12 個 AddOn + brandcards header 全部更新 ✅ 完成
6 Trait/interface alias wrappers:Trait_TMDO_Anti_EAV_Aware 等 ✅ 完成
7 Doctor callback signature 修正 ✅ 完成
8 打包驗收:scripts/package-plugin.sh 12 次 10 終檢 PASS ⬜ 待執行

#1 + #3 完成摘要(2026-05-15)

Unit tests:373/373 GREEN(771 assertions)

  • phpunit.xml 已建立,bootstrap tests/bootstrap.php 完整重建
  • 32 個 unit test 檔從 wp-data-optimizer 搬遷,全部 class-wpdo- → class-tmdo- 修正
  • bootstrap 新增:minimal filter registry($GLOBALS['_wp_filter_callbacks'])、全域 FSM bypass filter、TMDO_Listing_Stats stub、esc_sql 用 addslashes()
  • 跳過 HP/WC/LP 專屬 test(HivePress/*、WooCommerceIntegrationTest、ListingStatsTest)

Integration tests:398/398 GREEN(1139 assertions)

  • phpunit-integration.xml 已建立,bootstrap tests/integration/bootstrap.php 完整重建
  • 35 個 integration test 檔搬遷(跳過 HivepressIntegrationTest × 3、WCTermCountFilterTest、ListingMetaInterceptorTest、TermCommentBackfillTest)
  • bootstrap 新增:wp_upload_dir()、wp_mkdir_p()、TMDO_Listing_Stats stub
  • 移除 WarmArchiveIntegrationTest 的 listing_stats 2 個方法(需真實 HP AddOn)
  • 移除 RestApiIntegrationTest 的 post_view increment + rate-limit 2 個方法(同上)

Backport wp-data-optimizer v3.0.1–v3.4.6(2026-07-31 起)

完整計畫:~/.claude/plans/a-data-www-studio-wp-local-dev-wp-conten-imperative-wind.md

背景:B 分叉自 A v3.0.0(2026-05-15 11:40),A 其後又走了 92 個 commit 到 v3.4.6,無一回流。目標是全量對齊後讓 wp-data-optimizer 退休。

階段 內容 狀態
0 回歸網:composer dev 依賴、test bootstrap FSM guard、failOnWarning=true、12 個 repo git init ✅ 2026-07-31(unit 379 / integration 398 全綠)
1 安全與正確性 A1–A14(13 個 admin GET→POST、REST allowlist、DDL 白名單、Logger 3-arity fatal…) ✅ 2026-07-31
2 DDL 與並發 B1–B4(warm UNIQUE KEY → Zone_Cold save_patch → Zone_Warm increment) ✅ 2026-07-31
3 架構回填(Zone_Router + Routing_Predicate;13 個 Migration Phase 類別) ✅ 2026-07-31
4 相容層 E1–E7(雙向 hook 橋、57 個 AddOn alias、38 個 CLI 子指令、選單 slug) ✅ 2026-07-31
5 AddOn 同步 F1–F5(Standard_Post_Interceptor、HP detector v3.4.5、WC) ✅ 2026-07-31
6 功能與診斷 PR-G(crypto 健檢、doctor_callback ABI、15 個 usermeta 欄位…) ✅ 2026-07-31(Settings 分區 AJAX 未做,見下)
7 全域清理與 CI(strict_types、phpcs.xml、phpstan + baseline、gitea workflows) 🔶 strict_types / PHPCS / PHPStan / 3 個 workflow / ci-package.sh ✅;測試補齊(剩 36 檔,多為 HP/WC)、文件移植未做
退休 兩站台切到 B、移除 A、B 升 1.0.0 🔶 dev30 已切換並驗證通過;dev 未切、A 未移除、版本未升

dev30 實機驗證結果(2026-07-31)

環境:2meet-brandcards 1.8.3 / 2meet-hub-core 1.16.0 / 2meet-spoke-sso 1.15.0 同時啟用;HivePress / WooCommerce 未安裝,故只啟用核心 + hub-addon + spoke-addon。

項目 A v3.0.0(切換前) B v0.1.0(切換後)
doctor 33 個 [OK] + 15 個 WC 表 MISSING 警告 49 個 [OK]、All checks passed
姊妹外掛自訂表 36 張 OK 36 張 OK(含 brandcards 的 doctor_callback 回傳訊息)
新健檢 無 Backup dir blocked (HTTP 404)、Crypto key derivable 皆 OK

逐項確認:

  • alias:15 個核心類別 + trait + interface + wpdo_run() + WPDO_VERSION 全部可用
  • CLI:wp tmdo bridge-status / crypto-status / mode-audit 與 wp wpdo * 兩條路徑都通(38 個子指令)
  • anti-EAV 寫入:WPDO_API::set_entity() → wp_wpdo_user_sso.known_login_ips 寫入成功(A14 生效)
  • audit:wp_wpdo_audit 的 group_name / action / trace_id(UUIDv4)皆有值(A11/A12 生效)
  • REST allowlist:註冊 show_in_rest => false 的欄位不出現在 get_rest_visible_hot_columns()(A5/A6 生效)
  • doctor_callback ABI 變更:brandcards 既有 callback 在 1 參數簽章下仍正常
  • 前台 HTTP 200;_load_textdomain_just_in_time notice 僅出現在 wp plugin activate 當下(啟用鉤子早於 init),一般請求與前台皆無

實機才抓到的 fatal:TMDO_Logger::trace_id() 在 B 提煉時遺漏,而 Audit_Logger::write_row() 會呼叫它。A12 把 Audit_Logger 掛上後,任何一次受管 meta 寫入都會 fatal。PHPUnit 沒有覆蓋 audit 寫入路徑 → 這類「跨模組才會踩到」的缺口只有實機能發現。

尚未完成(follow-up)

  1. Settings 分區 AJAX 儲存(A v3.0.2):ajax_save_settings_section() + 7 個 save_section_*() + admin/assets/wpdo-settings.js。B 目前是一次存全部的 inline update_option 巨塊,功能可用,屬 UX 改善。其中 save_section_hp_transient / save_section_wc_term_count 對應 AddOn 已移除的設定,移植時要拆掉或加 class_exists 守衛。
  2. CI workflow ✅ 三個檔已建,但 B 沒有 git remote,要推上 gitea 才會實際執行;推上去後還需設 repo secret TMDO_TEST_DB_PASS、var RELEASE_GITEA_URL、secret RELEASE_TOKEN,並開 main branch protection。11 個 AddOn 的 CI 仍未建。
  3. scripts/ci-package.sh ✅(委派共用 package-plugin.sh,實測 10 項終檢全 PASS)
  4. 補測試檔 ✅ 核心補 HookBusIntegrationTest(14)+ 新寫 AuditLoggerIntegrationTest(4);HP AddOn 建 phpunit 基建並移植 25 檔(145 tests);WC AddOn 建基建並移植 1 檔(10 tests)。11 個 AddOn 皆有 CI workflow(HP/WC 跑 lint+unit,其餘 9 個薄註冊層只跑 lint)。
    • 仍缺:TermCommentBackfillTest(require HP AddOn 的 term-comment-fields,可移入 HP AddOn 套件)、ListingStatsTest(同上)、HP/WC 的 integration 測試(HivepressIntegrationTest ×3、WCTermCountFilterTest、ListingMetaInterceptorTest)——這些需要 AddOn 端的 integration bootstrap。

全家族測試現況(2026-07-31)

套件 結果
核心 unit 451 tests / 889 assertions
核心 integration 416 tests / 1165 assertions
hivepress-addon unit 145 tests / 357 assertions
woocommerce-addon unit 10 tests / 33 assertions

AddOn 測試 bootstrap 直接 require 核心 plugin 的 tests/bootstrap.php(避免複製 ~700 行 WP stub),因此 AddOn 的 CI job 必須同時 checkout 核心 plugin,workflow 已如此設定。 5. 文件移植:readme.txt、CONTEXT.md、docs/(ENTITY_ADAPTER_COOKBOOK、INTEGRATION_PATTERN_DECISION、2 篇 ADR)。 6. 實機驗證與退休切換:兩站台目前仍跑 A(dev = v3.4.6、dev30 = v3.0.0),B 家族 12 個外掛全 inactive,尚未做過 wp tmdo doctor 實機驗證。

ABI / 行為變更(升級須知)

  • doctor_callback 由 3 參數改為 1 參數(表名)——AddOn 若註冊過該 callback 需同步。
  • wpdo_capture_before_value 預設 true → false,每次受管寫入省一次 DB read;需要 value_before 的消費者(audit log)要 add_filter( 'wpdo_capture_before_value', '__return_true' )。
  • wpdo_allow_mass_column_clear 現為 opt-in:delete_all=true 的整欄清空預設被拒。
  • 選單 slug 由 2meet-data-optimizer 改回 wp-data-optimizer。

Lessons learned(backport)

  • 階段 0 的初始 git 快照已包含階段 0 的修復(composer.json / 兩個 bootstrap / phpunit.xml / HealthCronTest),不是純粹的 pre-change 現況。要看純現況請比對 A 或 dist/ 內的 v0.1.0 ZIP。
  • composer update 後 autoload 出現 WPDO_Entity_Adapter_Interface 三重宣告警告(includes/adapters/interface-entity-adapter.php、back-compat/interface-wpdo-entity-adapter-alias.php、back-compat/trait-wpdo-anti-eav-aware-alias.php)→ 階段 4 E2 要刪掉重複的那一份。
  • TMDO_FSM_GUARD_DISABLED 兩個 test bootstrap 原本都沒 define,但 class-tmdo-feature-flags.php:128 會檢查它 → 強制 FSM 轉換的測試會被 guard 擋掉(A 在 v3.4.6 踩過同一個坑)。
  • integration 測試需要 TMDO_TEST_DB_PASS(或 WPDO_TEST_DB_PASS)環境變數,DB 為本機 wp_wpdo_test。
  • 跑完 scripts/ci-package.sh 或 package-plugin.sh 後要重跑 composer install:打包流程內含 composer install --no-dev,會把 vendor/bin/{phpunit,phpcs,phpstan} 移掉,之後任何測試指令都會 "No such file or directory"。
  • A 不是照抄對象,有兩處自身缺陷不可照搬:
    1. A/includes/engine/class-wpdo-hook-bus.php:511,522 傳 3 個 string 給 Logger::warning( string $event, array $context ) → A 有 strict_types,mass-clear 路徑必炸。B 用正確簽章。
    2. A/admin/class-wpdo-dashboard-widget.php:191 的 wpdo_postmeta_cleanup 仍是 wp_nonce_url GET 連結,但其 handler 已只收 POST → 該按鈕在 A 是壞的。B 改成 form。
  • B 有 7 處 Logger::error( 'event', array(...) ) 兩參數呼叫(4 個 stress tester、auto-promoter、conflict-detector、hook-bus),全部是 array→string 的 TypeError。用 token 掃描器(scan_logger.php)找出來的,肉眼與 grep 都容易漏。
  • Zone_Warm::set() 改 upsert 後,測試表也要有 UNIQUE KEY ui_post_meta,否則 integration 會出現「set 沒覆蓋舊值」的假失敗。
  • B 的 unit bootstrap 會載入 Member_Fields / Post_Fields,A 的不會。這使 Entity_Registry 在測試啟動時就有註冊 group,從 A 移植過來、假設「registry 為空」的測試(MigrationPhaseRemainingTest)會真的走進 dbDelta 而爆掉。修法是在該測試 setUp 以 reflection 清空 registry 並移除 wpdo_register_entity_fields listener(phase 內部會再 fire 一次)。
  • 快取類重構必定連帶測試隔離工作:Routing_Predicate::flush_cache()(SyncBridgeEntityGuardTest)與 Schema_Manager::flush_table_exists_cache()(PostMigrationTest / PostEntityLifecycleTest)兩處都是 A 事後才補的,移植時要一起帶,否則出現「表被 drop 但快取說還在」的假錯誤。
  • B 自身的 slug 不一致:MENU_SLUG 是 2meet-data-optimizer,但 help-tabs / setup-wizard / conflict-monitor / 3 個 template 共 10+ 處硬編碼 page=wp-data-optimizer → 這些連結原本全部 404。已統一回 wp-data-optimizer(與 wpdo_ 表/option/cron/hook/CLI 命名空間一致)。
  • 雙向 hook 橋會暴露 test bootstrap 的 stub 缺口:單向時 do_action_ref_array() 從未被走到,改雙向後兩個 bootstrap 都要補這個 stub。
  • AddOn 的 alias 必須各自做:核心 back-compat 在 plugins_loaded:4 執行,AddOn 在 :6 才載入類別,核心的 alias 表涵蓋不到它們(57 個整合層類別先前完全沒有 WPDO_ 名稱)。
  • PHP 無法 class_alias() 介面。WPDO_Migration_Phase_Interface 沒有雙向 alias 的乾淨解法(單向 extends wrapper 無法讓 instanceof 成立),該契約是核心內部擴充點,測試改用 TMDO_ 正式名稱即可。

Git tag plan

  • v0.1.0 — 本 release(Phase 0-4 完成)
  • v0.1.1 — Phase 5 follow-up(tests / PHPCS / packaging)
  • v0.2.0 — WPDO_ deprecation notice + wpdo → tmdo migration CLI

NinjaFirewall 相容性 ✅(2026-08-15,v1.0.2)

計畫檔:~/.claude/plans/2meet-data-optimizer-ninjafirewall-effervescent-lighthouse.md

  • 評估與 NinjaFirewall 4.9 的相容性 → 不需要開發 AddOn
  • 診斷 Cannot retrieve user options from database (#3) → 與本外掛無關,根因為 symlink 部署
  • 建立 /var/www/Studio/.htninja(symlink-safe 站台錨點,全租戶共用)
  • dev30 啟用 ninjafirewall,補上缺失的 nfw_options / nfw_rules
  • TMDO_Options_Manager::PROTECTED_OPTIONS 守衛 + 4 個單元測試
  • Migration Wizard 輪詢 500ms → 2s
  • docs/WAF-COMPATIBILITY.md
  • 全套件回歸:591 tests / 1166 assertions OK

Lessons learned(WAF)

  • __DIR__ 會解析 symlink。NinjaFirewall Full WAF 用 dirname(dirname(dirname(__DIR__)))(lib/firewall.php:78)推導站台位置,在 symlink 共享 codebase 的 SaaS 架構下,所有租戶都會被判定成「共享目錄所屬的那個站」,於是連錯資料庫、撈不到自己的 nfw_options,回錯誤碼 6(訊息寫作 #3)。per-site 的可靠錨點是 $_SERVER['DOCUMENT_ROOT'](web server 的 root 指令值,不受 symlink 影響),這正是 .htninja 的搜尋基準。
  • .htninja 放在 dirname(DOCUMENT_ROOT) 可服務全部租戶(firewall.php:47-48 的第二順位),因為內容以 DOCUMENT_ROOT 動態推導,一份檔案通用。切勿在其中寫 return —— 'ALLOW' / 'BLOCK' 是有意義的回傳值。
  • NFW_LOG_DIR 對 WP WAF 模式同樣必要:L78 的誤判在兩種模式都存在,只是 WP WAF 不用它連 DB,但 log / cache / session 仍會全部寫進共享目錄互相覆蓋。
  • WAF 失效是靜默的。讀不到設定時 nfw_quit() 直接返回,不擋任何請求也不寫 log —— 「your site is not protected」是字面意思。因此任何會讓 nfw_options 離開 wp_options 的機制(例如本外掛的 register_settings_group() 重導向)都必須擋在註冊階段。
  • .user.ini 必須讓 php-fpm worker 可讀。dev32 那份是 root:root 0640,www-data 讀不到 → auto_prepend_file 靜默失效、Full WAF 等同沒裝。排查時容易誤判成「WAF 有在跑」,實際上擋下請求的是共享 mu-plugin 的 WP WAF。
  • 判斷 WAF 是否真的生效,要用會被規則擋的請求實測(例如 ?x=../../etc/passwd → 規則 1 → 403),光看首頁 200 或後台無錯誤訊息都不算數。
  • WP-CLI 完全豁免(firewall.php:18-23),所以 CLI 全綠不能當作「WAF 與外掛相容」的證據,必須另外走 HTTP 驗證。