Files
2meet-data-optimizer/PLAN.md
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

315 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PLAN — 2meet Data Optimizer (核心)
> 完整四階段拆分計畫見 `~/.claude/plans/wp-data-optimizer-2meet-data-optimizer-zesty-liskov.md`
---
## Current state
- **Version**: 1.0.2 (2026-08-15scaffold + 4 phase 完成於 2026-05-15)
- **Phase**: 全 4 phase 已完成
- **Source**: 從 `wp-data-optimizer v2.16.0` 提煉
---
## Phase 0 ✅ — 骨架建立 (2026-05-15)
- [x] 12 plugin 目錄結構(核心 + 11 AddOn
- [x] 主檔 `2meet-data-optimizer.php` + Plugin Header
- [x] `includes/class-tmdo-bootstrap.php` stub
- [x] `uninstall.php` stub
- [x] `composer.json`
- [x] `.gitignore`
- [x] 7 件 MDREADME / PLAN / CHANGELOG / DEPLOY / SECURITY / DESIGN / CLAUDE
---
## Phase 1 ✅ — Core v0.1.0 (2026-05-15)
- [x] rsync 從 wp-data-optimizer 複製 ~110 個核心檔案,排除 HP/WC/LP/2meet 整合
- [x] 檔名 `class-wpdo-*``class-tmdo-*`(含 abstract / interface / trait
- [x] sed 內容批次 rename`WPDO_``TMDO_`class + 常數,hook/table/option 前綴 `wpdo_` 保留共用)
- [x] 常數 `TMDO_PLUGIN_DIR``TMDO_PATH` 等對齊
- [x] Text-domain `'wp-data-optimizer'``'2meet-data-optimizer'`
- [x] 主檔重寫成完整 loaderrequire chain ~150 行)
- [x]`tmdo_run()` boot 函式 + `plugins_loaded:4` action
- [x]`late-bind safety net` (priority 30) 確保 AddOn listener 收到 hook
- [x]`includes/class-tmdo-back-compat.php` — 80+ class_alias + 8 常數 alias + 9 hook dual-fire
- [x] 移除 `register_hivepress_defaults()` (193 行) → 搬到 HP AddOn
- [x] 把 core 內所有 HP/WC/LP 整合 class 引用加 `class_exists` 守門
- [x] PHP lint111 files PASS
- [x] 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`)
- [x] `TMDO_Hub_Bootstrap` 主啟動類別
- [x] `TMDO_Hub_Vendor_Field` — 註冊 `_2meet_global_vendor_id` (hp_vendor / cold zone / indexed)
- [x] `TMDO_Hub_Custom_Tables` — 註冊 12 張 `2mhc_*` 自訂表(fallback 列表 + delegate to hub-core helper if available
- [x] dual-emit 支援 (`tmdo_*` + `wpdo_*` 兩個 hook 路徑)
- [x] PHP lint PASS
### Spoke AddOn (`2meet-data-optimizer-spoke-addon`)
- [x] `TMDO_Spoke_Bootstrap` 主啟動類別
- [x] `TMDO_Spoke_SSO_Group` — 註冊 user entity `sso` group7 欄位 → wp_wpdo_user_sso
- [x] `TMDO_Spoke_Custom_Tables` — 註冊 `2mso_sso_config` / `2mso_user_mapping`
- [x] `TMDO_Spoke_CLI::sync()` — 從舊 `_tmso_*` usermeta 遷移
- [x] `TMDO_Spoke_CLI::force_logout()` — 設置 token_expires_at = 過去
- [x] CLI 雙命名空間:`wp tmdo spoke-*` + `wp wpdo spoke-*`
- [x] 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 行)
- [x] 複製 13 adaptercore / reviews / bookings / messages / memberships / requests / favorites / statistics / tags / seo / social-links / blocks / marketplace
- [x] 複製 7 HPCT interceptorreviews / messages / favorites / memberships / statistics / requests / listing-meta
- [x] 複製 5 query interceptorreviews / messages / memberships / requests / listing-meta
- [x] 複製 補助組件(bootstrap framework / detector / conflict-guard / comment-router / cron-optimizer / attribute-bridge / suitability-scorer / benchmark / rest
- [x] 複製 admin tab + CLI namespace
- [x] 複製 HP transient filter / HPCT import / Listing Stats / term-comment-fields
- [x] `TMDO_HP_Bootstrap` outer bootstrap(避免與 inner family bootstrap class 衝突)
- [x] PHP lint46 files PASS
### WooCommerce AddOn (`2meet-data-optimizer-woocommerce-addon`, ~1,316 行)
- [x] 複製 4 個 WC 整合檔
- [x] `TMDO_Woocommerce_Bootstrap` 載入 + register
- [x] PHP lint PASS
### LatePoint AddOn (`2meet-data-optimizer-latepoint-addon`, ~151 行)
- [x] 複製 LatePoint interceptor
- [x] `TMDO_Latepoint_Bootstrap` 載入 + register_hooks
- [x] PHP lint PASS
**Phase 3 Live test 結果:**
- ✅ Plugin 全 active
- ✅ Hot zone 20 fields registeredmostly WC product/order
- ✅ Cold zone 3 fields
- ✅ Total 23 fields
- ✅ HP no-op when HivePress not installeddetector 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 表 | ✅ 註冊 OKDB 表 missing 是 spoke-sso 從未啟用之故) |
| WC 20 表 | ✅ 註冊 OKDB 表 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 而非 slughub-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` classv3.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 tests373/373 GREEN771 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 專屬 testHivePress/*、WooCommerceIntegrationTest、ListingStatsTest
**Integration tests398/398 GREEN1139 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.1v3.4.62026-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-31unit 379 / integration 398 全綠) |
| 1 | 安全與正確性 A1A1413 個 admin GET→POST、REST allowlist、DDL 白名單、Logger 3-arity fatal…) | ✅ 2026-07-31 |
| 2 | DDL 與並發 B1B4warm UNIQUE KEY → Zone_Cold save_patch → Zone_Warm increment | ✅ 2026-07-31 |
| 3 | 架構回填(Zone_Router + Routing_Predicate13 個 Migration Phase 類別) | ✅ 2026-07-31 |
| 4 | 相容層 E1E7(雙向 hook 橋、57 個 AddOn alias、38 個 CLI 子指令、選單 slug | ✅ 2026-07-31 |
| 5 | AddOn 同步 F1F5Standard_Post_Interceptor、HP detector v3.4.5、WC | ✅ 2026-07-31 |
| 6 | 功能與診斷 PR-Gcrypto 健檢、doctor_callback ABI、15 個 usermeta 欄位…) | ✅ 2026-07-31**Settings 分區 AJAX 未做**,見下) |
| 7 | 全域清理與 CIstrict_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 workflowHP/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. **實機驗證與退休切換**:兩站台目前仍跑 Adev = 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 learnedbackport
- **階段 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` — 本 releasePhase 0-4 完成)
- `v0.1.1` — Phase 5 follow-uptests / PHPCS / packaging
- `v0.2.0` — WPDO_ deprecation notice + wpdo → tmdo migration CLI
---
## NinjaFirewall 相容性 ✅(2026-08-15v1.0.2
計畫檔:`~/.claude/plans/2meet-data-optimizer-ninjafirewall-effervescent-lighthouse.md`
- [x] 評估與 NinjaFirewall 4.9 的相容性 → **不需要開發 AddOn**
- [x] 診斷 `Cannot retrieve user options from database (#3)` → 與本外掛無關,根因為 symlink 部署
- [x] 建立 `/var/www/Studio/.htninja`symlink-safe 站台錨點,全租戶共用)
- [x] dev30 啟用 ninjafirewall,補上缺失的 `nfw_options` / `nfw_rules`
- [x] `TMDO_Options_Manager::PROTECTED_OPTIONS` 守衛 + 4 個單元測試
- [x] Migration Wizard 輪詢 500ms → 2s
- [x] `docs/WAF-COMPATIBILITY.md`
- [x] 全套件回歸:591 tests / 1166 assertions OK
### Lessons learnedWAF
- **`__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 驗證。