f524ea3f16
- phpcs.xml(自 A 移植):ruleset 改名、*/tools/* 例外換成 */back-compat/*、 中文註解全域排除 Squiz.Commenting.InlineComment.InvalidEndChar、 interface 別名檔排除 OneObjectStructurePerFile - 檔頭正規化:16 個檔案的 declare(strict_types=1) 與前導 // 註解移到 file docblock 之後,並移除 <?php 後多餘空行(phpcbf 另自動修 190 處) - phpstan.neon + .phpstan/stubs.php(TMDO_ 與 WPDO_ 兩套常數)+ 重新產生的 phpstan-baseline.neon(710 errors,A 的 3877 行 baseline 因前綴與路徑不同無法沿用) 現況:PHPCS 0 errors / 0 warnings、PHPStan L6 No errors、 unit 451 / integration 398 GREEN Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
572 lines
19 KiB
PHP
572 lines
19 KiB
PHP
<?php
|
|
/**
|
|
* TMDO_CLI_Member — Member flat-table CLI subcommands.
|
|
*
|
|
* Adds the following subcommands under `wp wpdo`:
|
|
*
|
|
* wp wpdo member-audit — Mode, row counts, shadow diff rate.
|
|
* wp wpdo member-backfill — Backfill usermeta → flat table.
|
|
* wp wpdo member-shadow-report — Recent entity_type=user diff records.
|
|
* wp wpdo member-cutover — Switch user mode to aeav_only.
|
|
* wp wpdo member-points-check — Show balance + recent ledger for a user.
|
|
* wp wpdo member-sso-sync — Sync Hub membership claims to flat table.
|
|
* wp wpdo member-force-logout — Set token_expires_at to past → re-auth.
|
|
*
|
|
* @package WP_Data_Optimizer
|
|
* @since 2.5.5
|
|
*/
|
|
|
|
// phpcs:ignore WPDO.AntiEAV -- platform CLI inspector tool: raw meta queries needed for diagnostics
|
|
|
|
declare(strict_types=1);
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
// phpcs:disable Squiz.Commenting.FunctionComment.MissingParamTag,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.Commenting.DocComment.ShortNotCapital,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- WP_CLI callbacks accept ($args, $assoc_args) by contract; many subcommands only need one of them.
|
|
|
|
if ( ! class_exists( 'WP_CLI' ) ) {
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Member flat-table CLI subcommands. Loaded only in WP_CLI context.
|
|
*/
|
|
class TMDO_CLI_Member {
|
|
|
|
/**
|
|
* Valid group names managed by this CLI.
|
|
*
|
|
* V2.7.0: Extended with core_profile/social/commerce/hp_user to absorb
|
|
* the legacy WP/WC/HP wp_usermeta keys.
|
|
*/
|
|
private const VALID_GROUPS = array(
|
|
'membership',
|
|
'activity',
|
|
'profile',
|
|
'sso',
|
|
'core_profile',
|
|
'social',
|
|
'commerce',
|
|
'hp_user',
|
|
'admin_prefs',
|
|
);
|
|
|
|
// ── member-audit ─────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Show current user bridge mode, flat-table row counts, and shadow diff rate.
|
|
*
|
|
* ## EXAMPLES
|
|
*
|
|
* wp wpdo member-audit
|
|
*
|
|
* @param array $args Positional arguments (unused).
|
|
* @param array $assoc_args Named arguments (unused).
|
|
*/
|
|
public function member_audit( $args, $assoc_args ): void {
|
|
global $wpdb;
|
|
|
|
$mode = TMDO_Mode_Manager::get( 'user' );
|
|
WP_CLI::log( "User bridge mode: {$mode}" );
|
|
WP_CLI::log( '' );
|
|
|
|
$items = array();
|
|
foreach ( self::VALID_GROUPS as $group ) {
|
|
$table = $wpdb->prefix . 'wpdo_user_' . $group;
|
|
$exists = (bool) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
|
$wpdb->prepare( 'SHOW TABLES LIKE %s', $table )
|
|
);
|
|
$rows = $exists
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
? (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" )
|
|
: -1;
|
|
$items[] = array(
|
|
'group' => $group,
|
|
'table' => $table,
|
|
'exists' => $exists ? 'yes' : 'NO',
|
|
'rows' => $exists ? $rows : '(table missing)',
|
|
);
|
|
}
|
|
|
|
WP_CLI\Utils\format_items( 'table', $items, array( 'group', 'table', 'exists', 'rows' ) );
|
|
WP_CLI::log( '' );
|
|
|
|
// Shadow diff rate.
|
|
$shadow_table = $wpdb->prefix . 'wpdo_shadow_diffs';
|
|
$shadow_exists = (bool) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
|
$wpdb->prepare( 'SHOW TABLES LIKE %s', $shadow_table )
|
|
);
|
|
if ( $shadow_exists ) {
|
|
$total = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
|
"SELECT COUNT(*) FROM `{$shadow_table}` WHERE entity_type = 'user'" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
);
|
|
WP_CLI::log( "Shadow diffs (entity_type=user): {$total}" );
|
|
} else {
|
|
WP_CLI::log( 'Shadow diffs table: not present.' );
|
|
}
|
|
}
|
|
|
|
// ── member-backfill ───────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Backfill wp_usermeta rows into the specified user flat-table group.
|
|
*
|
|
* Uses TMDO_Entity_Migration_Engine::migrate_group() — cursor-based,
|
|
* 500 rows/batch, checkpoint stored in wpdo_migration_status.
|
|
*
|
|
* ## OPTIONS
|
|
*
|
|
* [--group=<group>]
|
|
* : Which group to backfill: membership, activity, profile, sso. Default: membership.
|
|
*
|
|
* [--batch-size=<n>]
|
|
* : Rows per batch (default 500, max 2000).
|
|
*
|
|
* [--dry-run]
|
|
* : Preview without writing.
|
|
*
|
|
* [--reset]
|
|
* : Clear checkpoint and restart from the beginning.
|
|
*
|
|
* ## EXAMPLES
|
|
*
|
|
* wp wpdo member-backfill --group=membership --dry-run
|
|
* wp wpdo member-backfill --group=membership
|
|
* wp wpdo member-backfill --group=sso --reset
|
|
*
|
|
* @param array $args Positional arguments.
|
|
* @param array $assoc_args Named arguments.
|
|
*/
|
|
public function member_backfill( $args, $assoc_args ): void {
|
|
$group = (string) ( $assoc_args['group'] ?? 'membership' );
|
|
$batch_size = min( 2000, max( 1, (int) ( $assoc_args['batch-size'] ?? 500 ) ) );
|
|
$dry_run = isset( $assoc_args['dry-run'] );
|
|
$reset = isset( $assoc_args['reset'] );
|
|
|
|
if ( ! in_array( $group, self::VALID_GROUPS, true ) ) {
|
|
WP_CLI::error( 'Invalid --group. Choose: ' . implode( ', ', self::VALID_GROUPS ) );
|
|
}
|
|
|
|
if ( $reset ) {
|
|
TMDO_Entity_Migration_Engine::reset_checkpoint( 'user', $group );
|
|
WP_CLI::log( "Checkpoint cleared for user/{$group}." );
|
|
}
|
|
|
|
WP_CLI::log( $dry_run ? "[dry-run] Backfill user/{$group} ..." : "Backfill user/{$group} ..." );
|
|
|
|
$result = TMDO_Entity_Migration_Engine::migrate_group(
|
|
'user',
|
|
$group,
|
|
array(
|
|
'batch_size' => $batch_size,
|
|
'dry_run' => $dry_run,
|
|
'resume' => ! $reset,
|
|
)
|
|
);
|
|
|
|
// Pre-flight failure (adapter/group/keys/table missing).
|
|
if ( ! empty( $result['error'] ) ) {
|
|
WP_CLI::error( $result['error'] );
|
|
}
|
|
|
|
$migrated = (int) ( $result['migrated'] ?? 0 );
|
|
$errors = (int) ( $result['errors'] ?? 0 );
|
|
$skipped = (int) ( $result['skipped'] ?? 0 );
|
|
|
|
// Row-level failures: migrate_group() catches Throwables per-entity, increments
|
|
// $stats['errors'], and continues. The returned $stats has no 'error' key,
|
|
// so without this branch the CLI would silently report success.
|
|
if ( $errors > 0 && 0 === $migrated ) {
|
|
WP_CLI::error(
|
|
sprintf(
|
|
'All %d row(s) failed during migration — see error_log for details.',
|
|
$errors
|
|
)
|
|
);
|
|
}
|
|
|
|
if ( $errors > 0 ) {
|
|
WP_CLI::warning(
|
|
sprintf( '%d row(s) failed during migration — see error_log for details.', $errors )
|
|
);
|
|
}
|
|
|
|
WP_CLI::success(
|
|
sprintf(
|
|
'Migrated %d rows, %d errors, %d skipped (%.2f sec)%s.',
|
|
$migrated,
|
|
$errors,
|
|
$skipped,
|
|
$result['elapsed_sec'] ?? 0,
|
|
$dry_run ? ' [dry-run, no changes written]' : ''
|
|
)
|
|
);
|
|
}
|
|
|
|
// ── member-shadow-report ──────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Display recent shadow_diffs rows for entity_type=user.
|
|
*
|
|
* ## OPTIONS
|
|
*
|
|
* [--limit=<n>]
|
|
* : Max rows to display (default 20).
|
|
*
|
|
* [--format=<format>]
|
|
* : Output format: table, json, csv. Default: table.
|
|
*
|
|
* ## EXAMPLES
|
|
*
|
|
* wp wpdo member-shadow-report
|
|
* wp wpdo member-shadow-report --limit=50 --format=json
|
|
*
|
|
* @param array $args Positional arguments.
|
|
* @param array $assoc_args Named arguments.
|
|
*/
|
|
public function member_shadow_report( $args, $assoc_args ): void {
|
|
global $wpdb;
|
|
|
|
$limit = min( 500, max( 1, (int) ( $assoc_args['limit'] ?? 20 ) ) );
|
|
$format = (string) ( $assoc_args['format'] ?? 'table' );
|
|
$table = $wpdb->prefix . 'wpdo_shadow_diffs';
|
|
|
|
$exists = (bool) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
|
$wpdb->prepare( 'SHOW TABLES LIKE %s', $table )
|
|
);
|
|
if ( ! $exists ) {
|
|
WP_CLI::warning( 'wpdo_shadow_diffs table not present — no shadow diffs collected yet.' );
|
|
return;
|
|
}
|
|
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
|
$rows = $wpdb->get_results(
|
|
$wpdb->prepare(
|
|
"SELECT id, entity_id, meta_key, postmeta_value, zone_value, diff_hash, ts FROM `{$table}` WHERE entity_type = 'user' ORDER BY ts DESC LIMIT %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
$limit
|
|
),
|
|
ARRAY_A
|
|
);
|
|
|
|
if ( empty( $rows ) ) {
|
|
WP_CLI::success( 'No shadow diffs found for entity_type=user.' );
|
|
return;
|
|
}
|
|
|
|
WP_CLI::log( count( $rows ) . " diff(s) found (limit {$limit}):" );
|
|
WP_CLI\Utils\format_items(
|
|
$format,
|
|
$rows,
|
|
array( 'id', 'entity_id', 'meta_key', 'diff_hash', 'ts' )
|
|
);
|
|
}
|
|
|
|
// ── member-cutover ────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Switch the user bridge mode to aeav_only (reads and writes go to flat table only).
|
|
*
|
|
* Requires --confirm to prevent accidental invocation.
|
|
* Recommended only after shadow diff rate drops below 0.1%.
|
|
*
|
|
* ## OPTIONS
|
|
*
|
|
* [--confirm]
|
|
* : Required confirmation flag.
|
|
*
|
|
* ## EXAMPLES
|
|
*
|
|
* wp wpdo member-cutover --confirm
|
|
*
|
|
* @param array $args Positional arguments.
|
|
* @param array $assoc_args Named arguments.
|
|
*/
|
|
public function member_cutover( $args, $assoc_args ): void {
|
|
if ( ! isset( $assoc_args['confirm'] ) ) {
|
|
WP_CLI::error( 'You must pass --confirm to cut over. Check shadow diff rate first: wp wpdo member-shadow-report' );
|
|
}
|
|
|
|
$current = TMDO_Mode_Manager::get( 'user' );
|
|
WP_CLI::log( "Current user mode: {$current}" );
|
|
|
|
if ( 'aeav_only' === $current ) {
|
|
WP_CLI::success( 'User mode is already aeav_only. Nothing to do.' );
|
|
return;
|
|
}
|
|
|
|
$result = TMDO_Mode_Manager::set( 'user', 'aeav_only' );
|
|
|
|
if ( isset( $result['error'] ) ) {
|
|
WP_CLI::error( 'Cutover failed: ' . $result['error'] );
|
|
}
|
|
|
|
WP_CLI::success( 'User bridge mode set to aeav_only. All user meta reads/writes now go through flat tables.' );
|
|
}
|
|
|
|
// ── member-points-check ───────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Show points balance and recent ledger entries for a user.
|
|
*
|
|
* ## OPTIONS
|
|
*
|
|
* <user_id>
|
|
* : WordPress user ID.
|
|
*
|
|
* [--limit=<n>]
|
|
* : Ledger rows to show (default 10).
|
|
*
|
|
* ## EXAMPLES
|
|
*
|
|
* wp wpdo member-points-check 42
|
|
* wp wpdo member-points-check 42 --limit=20
|
|
*
|
|
* @param array $args Positional arguments.
|
|
* @param array $assoc_args Named arguments.
|
|
*/
|
|
public function member_points_check( $args, $assoc_args ): void {
|
|
$user_id = (int) ( $args[0] ?? 0 );
|
|
if ( $user_id <= 0 ) {
|
|
WP_CLI::error( 'Usage: wp wpdo member-points-check <user_id>' );
|
|
}
|
|
|
|
$limit = min( 100, max( 1, (int) ( $assoc_args['limit'] ?? 10 ) ) );
|
|
|
|
$balance = TMDO_Points_Manager::get_balance( $user_id );
|
|
WP_CLI::log( "User {$user_id} — points balance: {$balance}" );
|
|
WP_CLI::log( '' );
|
|
|
|
$ledger = TMDO_Points_Manager::get_ledger( $user_id, $limit );
|
|
if ( empty( $ledger ) ) {
|
|
WP_CLI::log( 'No ledger entries found.' );
|
|
return;
|
|
}
|
|
|
|
WP_CLI\Utils\format_items(
|
|
'table',
|
|
$ledger,
|
|
array( 'id', 'delta', 'balance_after', 'reason', 'ref_type', 'ref_id', 'created_at' )
|
|
);
|
|
}
|
|
|
|
// ── member-sso-sync ───────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Sync Hub membership claims into the user flat table.
|
|
*
|
|
* Reads _tmso_refresh_token / _tmso_picture_url / _tmso_last_id_token
|
|
* from usermeta and writes them into the sso group flat table.
|
|
* Intended as a one-shot sync until 2meet-spoke-sso v1.14+ starts writing
|
|
* directly to the sso group via the Hook Bus.
|
|
*
|
|
* ## OPTIONS
|
|
*
|
|
* [--user-id=<id>]
|
|
* : Single user ID to sync.
|
|
*
|
|
* [--all]
|
|
* : Sync all users that have _tmso_refresh_token in usermeta (batch 500).
|
|
*
|
|
* [--dry-run]
|
|
* : Preview without writing.
|
|
*
|
|
* ## EXAMPLES
|
|
*
|
|
* wp wpdo member-sso-sync --user-id=42
|
|
* wp wpdo member-sso-sync --all --dry-run
|
|
*
|
|
* @param array $args Positional arguments.
|
|
* @param array $assoc_args Named arguments.
|
|
*/
|
|
public function member_sso_sync( $args, $assoc_args ): void {
|
|
global $wpdb;
|
|
|
|
$user_id = isset( $assoc_args['user-id'] ) ? (int) $assoc_args['user-id'] : 0;
|
|
$all = isset( $assoc_args['all'] );
|
|
$dry_run = isset( $assoc_args['dry-run'] );
|
|
|
|
if ( ! $user_id && ! $all ) {
|
|
WP_CLI::error( 'Provide --user-id=<id> or --all.' );
|
|
}
|
|
|
|
$sso_table = $wpdb->prefix . 'wpdo_user_sso';
|
|
$table_exists = (bool) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
|
$wpdb->prepare( 'SHOW TABLES LIKE %s', $sso_table )
|
|
);
|
|
if ( ! $table_exists ) {
|
|
WP_CLI::error( "SSO flat table {$sso_table} does not exist. Run `wp wpdo doctor` first." );
|
|
}
|
|
|
|
$user_ids = array();
|
|
if ( $user_id ) {
|
|
$user_ids = array( $user_id );
|
|
} else {
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
|
$user_ids = $wpdb->get_col(
|
|
"SELECT DISTINCT user_id FROM {$wpdb->usermeta} WHERE meta_key = '_tmso_refresh_token' LIMIT 5000" // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
|
);
|
|
}
|
|
|
|
if ( empty( $user_ids ) ) {
|
|
WP_CLI::log( 'No users with _tmso_refresh_token found.' );
|
|
return;
|
|
}
|
|
|
|
$synced = 0;
|
|
$skipped = 0;
|
|
$sso_keys = array(
|
|
'hub_global_user_id' => '_tmso_global_user_id',
|
|
'picture_url' => '_tmso_picture_url',
|
|
'refresh_token_enc' => '_tmso_refresh_token',
|
|
);
|
|
|
|
foreach ( $user_ids as $uid ) {
|
|
$uid = (int) $uid;
|
|
$data = array( 'user_id' => $uid );
|
|
|
|
foreach ( $sso_keys as $flat_col => $meta_key ) {
|
|
$val = get_user_meta( $uid, $meta_key, true );
|
|
if ( '' !== $val ) {
|
|
// Reject plaintext refresh tokens — must carry the enc_vN: envelope
|
|
// written by TMSO_Crypto::encrypt(). Storing cleartext here would
|
|
// silently bypass the encryption layer.
|
|
if ( 'refresh_token_enc' === $flat_col && ! preg_match( '/^enc_v\d+:/', (string) $val ) ) {
|
|
TMDO_Logger::error( 'sso_sync', 'plaintext_token_rejected', "user_id={$uid} meta_key={$meta_key}" );
|
|
continue;
|
|
}
|
|
$data[ $flat_col ] = $val;
|
|
}
|
|
}
|
|
|
|
if ( count( $data ) <= 1 ) {
|
|
++$skipped;
|
|
continue;
|
|
}
|
|
|
|
if ( ! $dry_run ) {
|
|
$wpdb->replace( $sso_table, $data ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
|
}
|
|
++$synced;
|
|
}
|
|
|
|
WP_CLI::success(
|
|
sprintf(
|
|
'SSO sync complete — synced: %d, skipped (no meta): %d%s.',
|
|
$synced,
|
|
$skipped,
|
|
$dry_run ? ' [dry-run]' : ''
|
|
)
|
|
);
|
|
}
|
|
|
|
// ── member-force-logout ───────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Force re-authentication by setting token_expires_at to a past datetime.
|
|
*
|
|
* The Spoke silent-refresh logic detects an expired token_expires_at and
|
|
* redirects the user to re-authenticate against the Hub. If the Hub has
|
|
* the account blocked, the user is logged out from all Spokes.
|
|
*
|
|
* ## OPTIONS
|
|
*
|
|
* [--user-id=<id>]
|
|
* : WordPress local user ID to force-logout.
|
|
*
|
|
* [--global-user-id=<uuid>]
|
|
* : Hub global_user_id to force-logout (looks up via hub_global_user_id column).
|
|
*
|
|
* [--confirm]
|
|
* : Required safety flag — confirms intent to invalidate live SSO tokens.
|
|
*
|
|
* ## EXAMPLES
|
|
*
|
|
* wp wpdo member-force-logout --user-id=42 --confirm
|
|
* wp wpdo member-force-logout --global-user-id=abc-123 --confirm
|
|
*
|
|
* @param array $args Positional arguments.
|
|
* @param array $assoc_args Named arguments.
|
|
*/
|
|
public function member_force_logout( $args, $assoc_args ): void {
|
|
if ( ! isset( $assoc_args['confirm'] ) ) {
|
|
WP_CLI::error( 'This invalidates live SSO tokens. Add --confirm to proceed.' );
|
|
}
|
|
|
|
global $wpdb;
|
|
|
|
$sso_table = $wpdb->prefix . 'wpdo_user_sso';
|
|
$past = '2000-01-01 00:00:00';
|
|
|
|
if ( isset( $assoc_args['user-id'] ) ) {
|
|
$uid = (int) $assoc_args['user-id'];
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
|
$updated = $wpdb->query(
|
|
$wpdb->prepare(
|
|
"UPDATE `{$sso_table}` SET token_expires_at = %s WHERE user_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
$past,
|
|
$uid
|
|
)
|
|
);
|
|
if ( $updated ) {
|
|
TMDO_Logger::info(
|
|
'member_force_logout',
|
|
array(
|
|
'user_id' => $uid,
|
|
'via' => 'user-id',
|
|
)
|
|
);
|
|
WP_CLI::success( "Force-logout applied to user_id={$uid}. Token invalidated." );
|
|
} else {
|
|
WP_CLI::warning( "No SSO row found for user_id={$uid}." );
|
|
}
|
|
return;
|
|
}
|
|
|
|
if ( isset( $assoc_args['global-user-id'] ) ) {
|
|
$global_id = sanitize_text_field( (string) $assoc_args['global-user-id'] );
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
|
$updated = $wpdb->query(
|
|
$wpdb->prepare(
|
|
"UPDATE `{$sso_table}` SET token_expires_at = %s WHERE hub_global_user_id = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
$past,
|
|
$global_id
|
|
)
|
|
);
|
|
if ( $updated ) {
|
|
TMDO_Logger::info(
|
|
'member_force_logout',
|
|
array(
|
|
'global_user_id' => $global_id,
|
|
'affected_rows' => $updated,
|
|
'via' => 'global-user-id',
|
|
)
|
|
);
|
|
WP_CLI::success( "Force-logout applied to global_user_id={$global_id}. Affected rows: {$updated}." );
|
|
} else {
|
|
WP_CLI::warning( "No SSO row found for global_user_id={$global_id}." );
|
|
}
|
|
return;
|
|
}
|
|
|
|
WP_CLI::error( 'Provide --user-id=<id> or --global-user-id=<uuid>.' );
|
|
}
|
|
}
|
|
|
|
// ── Register subcommands ──────────────────────────────────────────────────────
|
|
WP_CLI::add_command( 'wpdo member-audit', array( 'TMDO_CLI_Member', 'member_audit' ) );
|
|
WP_CLI::add_command( 'tmdo member-audit', array( 'TMDO_CLI_Member', 'member_audit' ) );
|
|
WP_CLI::add_command( 'wpdo member-backfill', array( 'TMDO_CLI_Member', 'member_backfill' ) );
|
|
WP_CLI::add_command( 'tmdo member-backfill', array( 'TMDO_CLI_Member', 'member_backfill' ) );
|
|
WP_CLI::add_command( 'wpdo member-shadow-report', array( 'TMDO_CLI_Member', 'member_shadow_report' ) );
|
|
WP_CLI::add_command( 'tmdo member-shadow-report', array( 'TMDO_CLI_Member', 'member_shadow_report' ) );
|
|
WP_CLI::add_command( 'wpdo member-cutover', array( 'TMDO_CLI_Member', 'member_cutover' ) );
|
|
WP_CLI::add_command( 'tmdo member-cutover', array( 'TMDO_CLI_Member', 'member_cutover' ) );
|
|
WP_CLI::add_command( 'wpdo member-points-check', array( 'TMDO_CLI_Member', 'member_points_check' ) );
|
|
WP_CLI::add_command( 'tmdo member-points-check', array( 'TMDO_CLI_Member', 'member_points_check' ) );
|
|
WP_CLI::add_command( 'wpdo member-sso-sync', array( 'TMDO_CLI_Member', 'member_sso_sync' ) );
|
|
WP_CLI::add_command( 'tmdo member-sso-sync', array( 'TMDO_CLI_Member', 'member_sso_sync' ) );
|
|
WP_CLI::add_command( 'wpdo member-force-logout', array( 'TMDO_CLI_Member', 'member_force_logout' ) );
|
|
WP_CLI::add_command( 'tmdo member-force-logout', array( 'TMDO_CLI_Member', 'member_force_logout' ) );
|