chore: initial snapshot of 2meet-data-optimizer-spoke-addon v0.1.0
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
||||
vendor/
|
||||
node_modules/
|
||||
.phpunit.result.cache
|
||||
.phpcs-cache
|
||||
*.log
|
||||
*.bak
|
||||
*.swp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.idea/
|
||||
.vscode/
|
||||
.full-review/
|
||||
.gstack/
|
||||
.playwright-mcp/
|
||||
.claude/
|
||||
dist/
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: 2meet Data Optimizer — Spoke AddOn
|
||||
* Plugin URI: https://2meet.io/2meet-data-optimizer/spoke
|
||||
* Description: Spoke SSO 整合:user sso group (7 欄位 → wp_wpdo_user_sso) + 2 mso 自訂表 + spoke-sync / spoke-force-logout CLI
|
||||
* Version: 0.1.0
|
||||
* Requires at least: 6.0
|
||||
* Tested up to: 6.9.4
|
||||
* Requires PHP: 8.1
|
||||
* Requires Plugins: 2meet-data-optimizer, 2meet-spoke-sso
|
||||
* Author: 2meet
|
||||
* Author URI: https://2meet.io
|
||||
* License: GPL-2.0-or-later
|
||||
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
* Text Domain: tmdo-spoke
|
||||
* Domain Path: /languages
|
||||
*
|
||||
* @package TMDO_SPOKE
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
define( 'TMDO_SPOKE_VERSION', '0.1.0' );
|
||||
define( 'TMDO_SPOKE_PATH', plugin_dir_path( __FILE__ ) );
|
||||
define( 'TMDO_SPOKE_URL', plugin_dir_url( __FILE__ ) );
|
||||
define( 'TMDO_SPOKE_FILE', __FILE__ );
|
||||
|
||||
require_once TMDO_SPOKE_PATH . 'includes/class-tmdo-spoke-bootstrap.php';
|
||||
|
||||
/**
|
||||
* Phase 2 任務:完成本 AddOn 的 includes/ 實作。
|
||||
*
|
||||
* @see ~/.claude/plans/wp-data-optimizer-2meet-data-optimizer-zesty-liskov.md
|
||||
*/
|
||||
add_action( 'plugins_loaded', array( 'TMDO_Spoke_Bootstrap', 'init' ), 6 );
|
||||
@@ -0,0 +1,22 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this AddOn will be documented here.
|
||||
|
||||
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
Versioning follows [Semantic Versioning](https://semver.org/).
|
||||
|
||||
---
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- Phase 2 implementation in progress.
|
||||
|
||||
---
|
||||
|
||||
## [0.1.0] — TBD
|
||||
|
||||
### Added (initial release, scaffold)
|
||||
|
||||
- Plugin scaffold from `wp-data-optimizer v2.16.0` extraction plan.
|
||||
- Plugin Header + bootstrap stub + 7 MD + composer.json.
|
||||
- Requires Plugins: `2meet-data-optimizer`, `2meet-spoke-sso`.
|
||||
@@ -0,0 +1,74 @@
|
||||
# CLAUDE.md — 2meet Data Optimizer Spoke AddOn
|
||||
|
||||
> 父外掛:`../2meet-data-optimizer/CLAUDE.md`
|
||||
> 父環境:`/var/www/Studio/wp-local-dev30/CLAUDE.md`
|
||||
> 全域:`~/.claude/CLAUDE.md`(含 Karpathy Guidelines)
|
||||
|
||||
---
|
||||
|
||||
## Reset recovery
|
||||
|
||||
1. 讀本檔
|
||||
2. 讀 [PLAN.md](PLAN.md) 看 Phase 2 進度
|
||||
3. 接續下一個未完成步驟
|
||||
|
||||
---
|
||||
|
||||
## Plugin 概述
|
||||
|
||||
**2meet-data-optimizer-spoke-addon** 是 2meet-data-optimizer 的 Spoke 整合 AddOn。
|
||||
|
||||
- **目標外掛**: `2meet-spoke-sso`
|
||||
- **Phase**: 2
|
||||
- **預估行數**: ~330
|
||||
- **Slug 短碼**: `spoke`
|
||||
|
||||
簡述:Spoke SSO 整合:user sso group (7 欄位) + 2 mso 自訂表 + 2 CLI 子指令
|
||||
|
||||
完整拆分計畫見 `~/.claude/plans/wp-data-optimizer-2meet-data-optimizer-zesty-liskov.md`。
|
||||
|
||||
---
|
||||
|
||||
## 邊界
|
||||
|
||||
- ✅ 本 AddOn 觸碰:spoke 相關 schema 註冊(postmeta zone fields / custom tables / entity groups)+ 可選 admin / CLI
|
||||
- ❌ 本 AddOn **不**觸碰:2meet-data-optimizer 核心、其他 AddOn、目標外掛 runtime 邏輯
|
||||
- ❌ 本 AddOn **不**繞過:Hook Bus / TMDO_API(必須走公開合約)
|
||||
|
||||
---
|
||||
|
||||
## 命名前綴
|
||||
|
||||
- 類別:`TMDO_Spoke_*`
|
||||
- 函式:`tmdo_spoke_*`
|
||||
- 常數:`TMDO_SPOKE_VERSION` / `TMDO_SPOKE_PATH` / `TMDO_SPOKE_URL` / `TMDO_SPOKE_FILE`
|
||||
- Text-domain:`tmdo-spoke`
|
||||
|
||||
---
|
||||
|
||||
## 公開合約消費(從核心)
|
||||
|
||||
```php
|
||||
// 安全消費 API(必加 class_exists guard)
|
||||
if ( class_exists( 'TMDO_API' ) ) {
|
||||
TMDO_API::set_entity( 'user', $id, 'key', $value );
|
||||
}
|
||||
|
||||
// Schema 註冊
|
||||
add_action( 'tmdo_register_entity_fields', function ( $registry_class ) {
|
||||
$registry_class::register_group( '...', '...', [ ... ] );
|
||||
} );
|
||||
|
||||
add_action( 'tmdo_register_custom_tables', function ( $registry ) {
|
||||
$registry->register( 'spoke-addon', [ ... ] );
|
||||
} );
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 進度
|
||||
|
||||
```
|
||||
Phase 0 ✅ 骨架建立 (2026-05-15)
|
||||
Phase 2 ⬜ Implementation v0.1.0
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
# 部署流程
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. `2meet-data-optimizer` 核心 ≥ 0.1.0 已安裝並啟用
|
||||
2. `2meet-spoke-sso` 已安裝並啟用
|
||||
|
||||
> 本 AddOn 透過 WordPress `Requires Plugins:` 強制相依,先決條件未滿足時無法啟用。
|
||||
|
||||
---
|
||||
|
||||
## Steps
|
||||
|
||||
```bash
|
||||
# 上傳 + 啟用
|
||||
wp plugin install /path/to/2meet-data-optimizer-spoke-addon-v0.1.0.zip --activate
|
||||
|
||||
# 驗證
|
||||
wp tmdo doctor --provider=spoke-addon
|
||||
wp tmdo custom-tables --provider=spoke-addon
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rollback
|
||||
|
||||
```bash
|
||||
wp plugin deactivate 2meet-data-optimizer-spoke-addon
|
||||
# 目標外掛 (2meet-spoke-sso) 仍可正常運作,
|
||||
# 讀寫 fall back 到 native meta(失去 zone 加速但功能不受影響)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 整體部署順序建議
|
||||
|
||||
依 Phase 順序:
|
||||
1. `2meet-data-optimizer` 核心
|
||||
2. Phase 2: `2meet-data-optimizer-hub-addon`、`2meet-data-optimizer-spoke-addon`
|
||||
3. Phase 3: HivePress / WooCommerce / LatePoint AddOn
|
||||
4. Phase 4: 6 個 2meet 家族 AddOn
|
||||
|
||||
詳見核心外掛 [DEPLOY.md](../2meet-data-optimizer/DEPLOY.md)。
|
||||
@@ -0,0 +1,44 @@
|
||||
# DESIGN — 2meet Data Optimizer Spoke AddOn
|
||||
|
||||
> 父外掛架構決策見 [`../2meet-data-optimizer/DESIGN.md`](../2meet-data-optimizer/DESIGN.md)
|
||||
|
||||
---
|
||||
|
||||
## 角色定位
|
||||
|
||||
本 AddOn 是 `2meet-data-optimizer` 的整合層,負責將 `2meet-spoke-sso` 的 EAV 模式(postmeta / 自訂表)橋接到核心反 EAV 引擎。
|
||||
|
||||
- **不**修改 `2meet-spoke-sso` 本身
|
||||
- **不**繞過 `2meet-data-optimizer` 核心
|
||||
- 只透過公開合約(`TMDO_API`、`tmdo_register_*` hook)參與
|
||||
|
||||
---
|
||||
|
||||
## 啟動順序
|
||||
|
||||
```
|
||||
plugins_loaded:4 2meet-data-optimizer (core) 啟動
|
||||
plugins_loaded:5-6 本 AddOn bootstrap → 註冊 hook callback
|
||||
plugins_loaded:6+ 2meet-spoke-sso 本體啟動
|
||||
init:0 核心 fire tmdo_register_* → 本 AddOn 的 callback 執行
|
||||
```
|
||||
|
||||
詳見 plan 文件第 3 章「載入順序」。
|
||||
|
||||
---
|
||||
|
||||
## 為什麼是獨立 AddOn 而非塞進核心
|
||||
|
||||
- 核心 `2meet-data-optimizer` 對外承諾「零依存任何業務外掛」
|
||||
- 整合層發版節奏應跟隨 `2meet-spoke-sso` 而非核心
|
||||
- 站台不用 `2meet-spoke-sso` 時不需被迫載入這段程式碼
|
||||
|
||||
---
|
||||
|
||||
## 邊界檢查清單
|
||||
|
||||
- [ ] 不直接 `SELECT FROM wp_*meta`
|
||||
- [ ] 不繞過 Hook Bus
|
||||
- [ ] 所有 SQL 走 `$wpdb->prepare()`
|
||||
- [ ] DB 表名走 `TMDO_DB::table()`
|
||||
- [ ] 對外只 expose `TMDO_Spoke_*` 命名空間
|
||||
@@ -0,0 +1,43 @@
|
||||
# PLAN — 2meet Data Optimizer Spoke AddOn
|
||||
|
||||
> 完整四階段拆分計畫見 `~/.claude/plans/wp-data-optimizer-2meet-data-optimizer-zesty-liskov.md` (本 AddOn 屬 **Phase 2**)
|
||||
|
||||
---
|
||||
|
||||
## Current state
|
||||
|
||||
- **Version**: 0.1.0 (in development, scaffold complete 2026-05-15)
|
||||
- **Phase**: Phase 0 → Phase 2 pending
|
||||
- **Source**: 從 `wp-data-optimizer v2.16.0` 提煉
|
||||
- **預估行數**: ~330 行
|
||||
- **目標外掛**: `2meet-spoke-sso`
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 ✅ — 骨架建立 (2026-05-15)
|
||||
|
||||
- [x] 目錄結構
|
||||
- [x] Plugin Header
|
||||
- [x] Bootstrap stub
|
||||
- [x] 7 件 MD
|
||||
- [x] composer.json / .gitignore / uninstall.php
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 ⬜ — Implementation v0.1.0
|
||||
|
||||
詳細任務見上層 plan 第 5 章。簡述:
|
||||
|
||||
- [ ] 從 wp-data-optimizer 對應檔案搬入 includes/
|
||||
- [ ] WPDO_ → TMDO_Spoke_ 重命名
|
||||
- [ ] 移除對 wp-data-optimizer 內部 class 的依存(改走 TMDO_API 公開合約)
|
||||
- [ ] 完善 bootstrap:實作 init() 載入順序
|
||||
- [ ] 補完 7 件 MD 的細節
|
||||
- [ ] 跑 `scripts/package-plugin.sh 2meet-data-optimizer-spoke-addon` 通過 10 終檢
|
||||
- [ ] dev30 e2e
|
||||
|
||||
---
|
||||
|
||||
## Lessons learned
|
||||
|
||||
- (Phase 2 起記錄)
|
||||
@@ -0,0 +1,76 @@
|
||||
# 2meet Data Optimizer — Spoke SSO AddOn
|
||||
|
||||
> 為 **2meet-spoke-sso**(OAuth client / SSO 客戶端)提供高效能 token cache 與 SLO 整合。
|
||||
|
||||
---
|
||||
|
||||
## What it does
|
||||
|
||||
把 spoke-sso 對 `wp-data-optimizer` 的依存抽出來:
|
||||
|
||||
| 項目 | 內容 |
|
||||
|---|---|
|
||||
| User entity group | `sso` 群組 7 欄位 → flat table `wp_wpdo_user_sso`(取代 `_tmso_*` usermeta EAV)|
|
||||
| Custom tables | 2 張 mso 自訂表(`2mso_sso_config` / `2mso_user_mapping`)→ doctor 健診 |
|
||||
| CLI | `wp tmdo spoke-sync`(從舊 _tmso_* usermeta 遷移)/ `wp tmdo spoke-force-logout`(強制 token 過期)|
|
||||
| Crypto | TMSO_Crypto interop wrapper(refresh_token AES-256-CBC encrypted)|
|
||||
|
||||
---
|
||||
|
||||
## SSO Group 欄位
|
||||
|
||||
| 欄位 | Type | Searchable | 用途 |
|
||||
|---|---|:-:|---|
|
||||
| `hub_global_user_id` | text | ✓ | Hub 全局 user UUID(`2mso_user_mapping.global_user_id` bridge key) |
|
||||
| `picture_url` | text | | SSO 大頭貼 URL |
|
||||
| `last_id_token_hash` | text | | SHA-256(last_id_token) for SLO 比較(不存明文,A-4 修復)|
|
||||
| `refresh_token_enc` | textarea | | 加密 refresh token (`enc_vN:ciphertext`) |
|
||||
| `token_expires_at` | datetime | ✓ | SSO token 過期時間(設過去可強制重新認證)|
|
||||
| `sso_last_login_at` | datetime | | 上次 SSO 登入時間 |
|
||||
| `sso_login_count` | integer | | SSO 登入次數 |
|
||||
|
||||
設計原因:silent refresh 每 15 min 觸發一次,10M user 場景下 wp_usermeta EAV scan 會成熱點;flat table + object cache 命中可降低 DB load 10-50×。
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
- WordPress ≥ 6.0
|
||||
- PHP ≥ 8.1
|
||||
- 2meet-data-optimizer ≥ 0.1.0
|
||||
- 2meet-spoke-sso ≥ 1.15.0
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
wp plugin install /path/to/2meet-data-optimizer-spoke-addon-v0.1.0.zip --activate
|
||||
|
||||
# 驗證 sso group 已註冊
|
||||
wp tmdo entity-fields --entity=user --group=sso
|
||||
|
||||
# 從舊 _tmso_* usermeta 遷移到 flat table
|
||||
wp tmdo spoke-sync --dry-run
|
||||
wp tmdo spoke-sync --batch-size=500
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fallback
|
||||
|
||||
若本 AddOn 未啟用,spoke-sso 仍能透過 native `update_user_meta()` 儲存 SSO 資訊(透過 `_tmso_*` usermeta 路徑),效能下降但功能不影響。
|
||||
|
||||
---
|
||||
|
||||
## Phase
|
||||
|
||||
Phase 2 — 與 [Hub AddOn](../2meet-data-optimizer-hub-addon/) 同期 ship。
|
||||
|
||||
詳見 [PLAN.md](PLAN.md) 與 [DESIGN.md](DESIGN.md)。
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
GPL-2.0-or-later
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported versions
|
||||
|
||||
| Version | Supported |
|
||||
|---|:-:|
|
||||
| 0.1.x | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
請參考核心外掛 [SECURITY.md](../2meet-data-optimizer/SECURITY.md)。
|
||||
|
||||
簡述:
|
||||
- Email: security@2meet.io
|
||||
- 請勿在 public GitHub issue 揭露
|
||||
- 24h 內初步回應、7 天內提供修補時程
|
||||
|
||||
---
|
||||
|
||||
## 安全設計
|
||||
|
||||
本 AddOn 完全遵守 2meet-data-optimizer 核心安全設計:
|
||||
|
||||
- SQL prepared statements
|
||||
- Capability check (`manage_options`)
|
||||
- Nonce 驗證
|
||||
- Output escaping
|
||||
- 反序列化走 `TMDO_Safe_Unserialize`
|
||||
|
||||
---
|
||||
|
||||
## 已知 Risk
|
||||
|
||||
- (本版本暫無已知安全 risk;Phase 2 ship 時補)
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
/**
|
||||
* WP-CLI commands for spoke-sso AddOn.
|
||||
*
|
||||
* 從 wp-data-optimizer/cli/class-wpdo-cli-member.php 的 sso-sync / force-logout 抽出。
|
||||
*
|
||||
* @package TMDO_SPOKE
|
||||
* @since 0.1.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
final class TMDO_Spoke_CLI {
|
||||
|
||||
/**
|
||||
* 從舊 _tmso_* usermeta 同步到 wp_wpdo_user_sso flat table。
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--user-id=<id>]
|
||||
* : 限定單一 user 同步(預設全部)
|
||||
*
|
||||
* [--batch-size=<n>]
|
||||
* : 每批處理筆數(預設 500)
|
||||
*
|
||||
* [--dry-run]
|
||||
* : 只顯示會做什麼,不實際寫入
|
||||
*
|
||||
* @param array $args
|
||||
* @param array $assoc_args
|
||||
*/
|
||||
public static function sync( $args, $assoc_args ): void {
|
||||
$user_id = isset( $assoc_args['user-id'] ) ? absint( $assoc_args['user-id'] ) : 0;
|
||||
$batch = isset( $assoc_args['batch-size'] ) ? max( 1, absint( $assoc_args['batch-size'] ) ) : 500;
|
||||
$dry_run = ! empty( $assoc_args['dry-run'] );
|
||||
|
||||
if ( ! class_exists( 'TMDO_API' ) && ! class_exists( 'WPDO_API' ) ) {
|
||||
\WP_CLI::error( '需要 2meet-data-optimizer 核心啟用' );
|
||||
}
|
||||
$api = class_exists( 'TMDO_API' ) ? 'TMDO_API' : 'WPDO_API';
|
||||
|
||||
global $wpdb;
|
||||
$where = $user_id ? $wpdb->prepare( 'AND user_id = %d', $user_id ) : '';
|
||||
// 支援 _tmso_global_user_id / _tmso_picture_url / _tmso_refresh_token 三個舊 key
|
||||
$rows = $wpdb->get_results(
|
||||
"SELECT user_id, meta_key, meta_value FROM {$wpdb->usermeta}
|
||||
WHERE meta_key IN ('_tmso_global_user_id','_tmso_picture_url','_tmso_refresh_token')
|
||||
$where
|
||||
LIMIT $batch",
|
||||
ARRAY_A
|
||||
);
|
||||
if ( empty( $rows ) ) {
|
||||
\WP_CLI::success( '無待同步資料' );
|
||||
return;
|
||||
}
|
||||
|
||||
$migrated = 0;
|
||||
$key_map = array(
|
||||
'_tmso_global_user_id' => 'hub_global_user_id',
|
||||
'_tmso_picture_url' => 'picture_url',
|
||||
'_tmso_refresh_token' => 'refresh_token_enc',
|
||||
);
|
||||
|
||||
foreach ( $rows as $row ) {
|
||||
$dst_key = $key_map[ $row['meta_key'] ] ?? null;
|
||||
if ( ! $dst_key ) {
|
||||
continue;
|
||||
}
|
||||
if ( $dry_run ) {
|
||||
\WP_CLI::log( "[dry-run] user={$row['user_id']} {$row['meta_key']} → {$dst_key}" );
|
||||
} else {
|
||||
$api::set_entity( 'user', (int) $row['user_id'], $dst_key, $row['meta_value'] );
|
||||
}
|
||||
$migrated++;
|
||||
}
|
||||
|
||||
\WP_CLI::success( ( $dry_run ? '[dry-run] would migrate ' : 'migrated ' ) . $migrated . ' rows' );
|
||||
}
|
||||
|
||||
/**
|
||||
* 強制使用者 SSO token 過期(設 token_expires_at 為過去時間)。
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--user-id=<id>]
|
||||
* : Local user ID
|
||||
*
|
||||
* [--global-user-id=<uuid>]
|
||||
* : Hub global user UUID
|
||||
*
|
||||
* [--all]
|
||||
* : 強制全部 user 重認
|
||||
*
|
||||
* [--dry-run]
|
||||
* : 只顯示會做什麼
|
||||
*
|
||||
* @param array $args
|
||||
* @param array $assoc_args
|
||||
*/
|
||||
public static function force_logout( $args, $assoc_args ): void {
|
||||
if ( ! class_exists( 'TMDO_API' ) && ! class_exists( 'WPDO_API' ) ) {
|
||||
\WP_CLI::error( '需要 2meet-data-optimizer 核心啟用' );
|
||||
}
|
||||
$api = class_exists( 'TMDO_API' ) ? 'TMDO_API' : 'WPDO_API';
|
||||
|
||||
$dry_run = ! empty( $assoc_args['dry-run'] );
|
||||
$user_id = isset( $assoc_args['user-id'] ) ? absint( $assoc_args['user-id'] ) : 0;
|
||||
$global_uuid = isset( $assoc_args['global-user-id'] ) ? sanitize_text_field( $assoc_args['global-user-id'] ) : '';
|
||||
$all = ! empty( $assoc_args['all'] );
|
||||
|
||||
if ( ! $user_id && ! $global_uuid && ! $all ) {
|
||||
\WP_CLI::error( '需指定 --user-id / --global-user-id / --all 其中之一' );
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$past = '2000-01-01 00:00:00';
|
||||
$ids = array();
|
||||
|
||||
if ( $user_id ) {
|
||||
$ids[] = $user_id;
|
||||
} elseif ( $global_uuid ) {
|
||||
$flat_table = $wpdb->prefix . 'wpdo_user_sso';
|
||||
$id = $wpdb->get_var( $wpdb->prepare(
|
||||
"SELECT user_id FROM $flat_table WHERE hub_global_user_id = %s LIMIT 1",
|
||||
$global_uuid
|
||||
) );
|
||||
if ( $id ) {
|
||||
$ids[] = (int) $id;
|
||||
}
|
||||
} elseif ( $all ) {
|
||||
$flat_table = $wpdb->prefix . 'wpdo_user_sso';
|
||||
$rows = $wpdb->get_col( "SELECT user_id FROM $flat_table" );
|
||||
$ids = array_map( 'intval', $rows );
|
||||
}
|
||||
|
||||
if ( empty( $ids ) ) {
|
||||
\WP_CLI::warning( '找不到符合條件的 user' );
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $ids as $uid ) {
|
||||
if ( $dry_run ) {
|
||||
\WP_CLI::log( "[dry-run] user={$uid} token_expires_at → {$past}" );
|
||||
} else {
|
||||
$api::set_entity( 'user', $uid, 'token_expires_at', $past );
|
||||
}
|
||||
}
|
||||
|
||||
\WP_CLI::success( ( $dry_run ? '[dry-run] would force-logout ' : 'force-logout ' ) . count( $ids ) . ' users' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "2meet/2meet-data-optimizer-spoke-addon",
|
||||
"description": "Spoke SSO 整合:user sso group (7 欄位) + 2 mso 自訂表 + 2 CLI 子指令",
|
||||
"type": "wordpress-plugin",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"keywords": ["wordpress", "2meet-data-optimizer", "addon", "spoke"],
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.5",
|
||||
"wp-coding-standards/wpcs": "^3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"phpcs": "phpcs --standard=WordPress includes/ 2meet-data-optimizer-spoke-addon.php"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": true
|
||||
},
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true
|
||||
}
|
||||
}
|
||||
Generated
+2106
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
/**
|
||||
* Bootstrap for 2meet Data Optimizer Spoke SSO AddOn.
|
||||
*
|
||||
* 負責 spoke-sso 對 2meet-data-optimizer 反 EAV 引擎的 schema 整合:
|
||||
* - 註冊 user entity group `sso`(7 欄位 → wp_wpdo_user_sso flat table)
|
||||
* - 註冊 spoke-sso 2 張自訂表(2mso_sso_config / 2mso_user_mapping)
|
||||
* - 提供 spoke-sync / spoke-force-logout CLI
|
||||
*
|
||||
* @package TMDO_SPOKE
|
||||
* @since 0.1.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once TMDO_SPOKE_PATH . 'includes/class-tmdo-spoke-sso-group.php';
|
||||
require_once TMDO_SPOKE_PATH . 'includes/class-tmdo-spoke-custom-tables.php';
|
||||
|
||||
final class TMDO_Spoke_Bootstrap {
|
||||
|
||||
public static function init(): void {
|
||||
if ( ! class_exists( 'TMDO_API' ) && ! class_exists( 'WPDO_API' ) ) {
|
||||
add_action( 'admin_notices', array( __CLASS__, 'missing_core_notice' ) );
|
||||
return;
|
||||
}
|
||||
|
||||
load_plugin_textdomain(
|
||||
'tmdo-spoke',
|
||||
false,
|
||||
dirname( plugin_basename( TMDO_SPOKE_FILE ) ) . '/languages'
|
||||
);
|
||||
|
||||
add_action( 'tmdo_register_entity_fields', array( 'TMDO_Spoke_SSO_Group', 'register' ), 10, 1 );
|
||||
add_action( 'wpdo_register_entity_fields', array( 'TMDO_Spoke_SSO_Group', 'register' ), 10, 1 );
|
||||
|
||||
add_action( 'tmdo_register_custom_tables', array( 'TMDO_Spoke_Custom_Tables', 'register' ), 10, 1 );
|
||||
add_action( 'wpdo_register_custom_tables', array( 'TMDO_Spoke_Custom_Tables', 'register' ), 10, 1 );
|
||||
|
||||
// CLI 命令延後到 cli init(class WP_CLI 存在時)
|
||||
if ( defined( 'WP_CLI' ) && WP_CLI ) {
|
||||
require_once TMDO_SPOKE_PATH . 'cli/class-tmdo-spoke-cli.php';
|
||||
\WP_CLI::add_command( 'tmdo spoke-sync', array( 'TMDO_Spoke_CLI', 'sync' ) );
|
||||
\WP_CLI::add_command( 'tmdo spoke-force-logout', array( 'TMDO_Spoke_CLI', 'force_logout' ) );
|
||||
// 向後相容:對外仍提供 wp wpdo spoke-* 路徑。
|
||||
\WP_CLI::add_command( 'wpdo spoke-sync', array( 'TMDO_Spoke_CLI', 'sync' ) );
|
||||
\WP_CLI::add_command( 'wpdo spoke-force-logout', array( 'TMDO_Spoke_CLI', 'force_logout' ) );
|
||||
}
|
||||
}
|
||||
|
||||
public static function missing_core_notice(): void {
|
||||
echo '<div class="notice notice-error"><p>';
|
||||
echo esc_html__( '2meet Data Optimizer Spoke AddOn 需要 2meet-data-optimizer 核心外掛,請先安裝並啟用。', 'tmdo-spoke' );
|
||||
echo '</p></div>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
/**
|
||||
* Register spoke-sso 2 custom tables to TMDO Custom Table Registry.
|
||||
*
|
||||
* @package TMDO_SPOKE
|
||||
* @since 0.1.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
final class TMDO_Spoke_Custom_Tables {
|
||||
|
||||
private static bool $registered = false;
|
||||
|
||||
private const TABLES = array(
|
||||
'2mso_sso_config' => array(
|
||||
'description' => 'spoke-sso Hub OAuth2 連線設定',
|
||||
),
|
||||
'2mso_user_mapping' => array(
|
||||
'description' => 'spoke-sso Hub global_user_id ↔ local user_id mapping',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Register tables to TMDO Custom Table Registry.
|
||||
*
|
||||
* @param object $registry TMDO_Custom_Table_Registry / WPDO_Custom_Table_Registry instance.
|
||||
*/
|
||||
public static function register( $registry ): void {
|
||||
if ( self::$registered ) {
|
||||
return;
|
||||
}
|
||||
if ( ! is_object( $registry ) || ! method_exists( $registry, 'register' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( self::TABLES as $suffix => $meta ) {
|
||||
$registry->register(
|
||||
'2meet-data-optimizer-spoke-addon',
|
||||
array(
|
||||
'table_name' => $suffix,
|
||||
'primary_key' => 'id',
|
||||
'description' => $meta['description'],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
self::$registered = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
/**
|
||||
* SSO entity group registration(7 欄位).
|
||||
*
|
||||
* 從 wp-data-optimizer/includes/integrations/class-wpdo-member-fields.php:226-271 抽出。
|
||||
* 註冊到 user entity,flat table = wp_wpdo_user_sso。
|
||||
*
|
||||
* @package TMDO_SPOKE
|
||||
* @since 0.1.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
final class TMDO_Spoke_SSO_Group {
|
||||
|
||||
private static bool $registered = false;
|
||||
|
||||
/**
|
||||
* Register `sso` group on user entity.
|
||||
*
|
||||
* @param string|object $registry_class TMDO_Entity_Registry / WPDO_Entity_Registry class name or instance.
|
||||
*/
|
||||
public static function register( $registry_class ): void {
|
||||
if ( self::$registered ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析 registry:可能是 class name (string) 或 instance。
|
||||
$cls = is_string( $registry_class ) ? $registry_class
|
||||
: ( is_object( $registry_class ) ? get_class( $registry_class ) : null );
|
||||
|
||||
if ( ! $cls || ! class_exists( $cls ) || ! method_exists( $cls, 'register_group' ) ) {
|
||||
// fallback:直接呼叫已知 class
|
||||
if ( class_exists( 'TMDO_Entity_Registry' ) ) {
|
||||
$cls = 'TMDO_Entity_Registry';
|
||||
} elseif ( class_exists( 'WPDO_Entity_Registry' ) ) {
|
||||
$cls = 'WPDO_Entity_Registry';
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$cls::register_group(
|
||||
'user',
|
||||
'sso',
|
||||
array(
|
||||
array(
|
||||
'key' => 'hub_global_user_id',
|
||||
'type' => 'text',
|
||||
'searchable' => true,
|
||||
'label' => 'Hub global user UUID (2mso_user_mapping.global_user_id bridge key)',
|
||||
),
|
||||
array(
|
||||
'key' => 'picture_url',
|
||||
'type' => 'text',
|
||||
'label' => 'Social / SSO profile picture URL',
|
||||
),
|
||||
array(
|
||||
'key' => 'last_id_token_hash',
|
||||
'type' => 'text',
|
||||
'label' => 'SHA-256(last_id_token) for SLO comparison — no plaintext stored',
|
||||
),
|
||||
array(
|
||||
'key' => 'refresh_token_enc',
|
||||
'type' => 'textarea',
|
||||
'label' => 'Encrypted refresh token (TMSO_Crypto — key-versioned enc_vN:ciphertext)',
|
||||
),
|
||||
array(
|
||||
'key' => 'token_expires_at',
|
||||
'type' => 'datetime',
|
||||
'searchable' => true,
|
||||
'label' => 'SSO token expiry (set to past to force re-auth on next request)',
|
||||
),
|
||||
array(
|
||||
'key' => 'sso_last_login_at',
|
||||
'type' => 'datetime',
|
||||
'label' => 'Last SSO-initiated login datetime',
|
||||
),
|
||||
array(
|
||||
'key' => 'sso_login_count',
|
||||
'type' => 'integer',
|
||||
'default' => 0,
|
||||
'label' => 'SSO login count',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
self::$registered = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
/**
|
||||
* Uninstall handler for 2meet Data Optimizer Spoke AddOn.
|
||||
*
|
||||
* 僅清理本 AddOn 自有 schema(核心 wp_wpdo_* 系統表保留給 core)。
|
||||
*
|
||||
* @package TMDO_SPOKE
|
||||
*/
|
||||
|
||||
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Phase 2 完整實作。Phase 0 stub:保留所有資料以避免誤刪。
|
||||
Reference in New Issue
Block a user