diff --git a/admin/class-tmdo-deprecated-addons-notice.php b/admin/class-tmdo-deprecated-addons-notice.php index a49f72b..ecc665a 100644 --- a/admin/class-tmdo-deprecated-addons-notice.php +++ b/admin/class-tmdo-deprecated-addons-notice.php @@ -6,7 +6,8 @@ * mobile-bridge, playlist, quotation, spoke) only registered tables and fields * on the partner's behalf. Partner plugins now register themselves with the * TMDO_Anti_EAV_Aware trait (docs/PARTNER-REGISTRATION.md), so these AddOns - * are deprecated and will be removed. + * are deprecated and will be removed. The hub and spoke AddOns are replaced + * by 2meet-data-optimizer-sso-addon. * * @package WP_Data_Optimizer */ @@ -79,5 +80,11 @@ final class TMDO_Deprecated_Addons_Notice { ), esc_html__( '改寫方式見 2meet-data-optimizer 外掛內的 docs/PARTNER-REGISTRATION.md;partner 外掛完成自行註冊後即可停用對應 AddOn。', '2meet-data-optimizer' ) ); + if ( array_intersect( $active, array( 'hub', 'spoke' ) ) ) { + printf( + '

%s

', + esc_html__( 'Hub / Spoke AddOn 由 2meet-data-optimizer-sso-addon 取代:啟用它之後即可停用這兩個 AddOn。', '2meet-data-optimizer' ) + ); + } } } diff --git a/includes/class-tmdo-compatibility.php b/includes/class-tmdo-compatibility.php index c8891eb..cda622f 100644 --- a/includes/class-tmdo-compatibility.php +++ b/includes/class-tmdo-compatibility.php @@ -85,6 +85,10 @@ class TMDO_Compatibility { if ( self::is_woocommerce_active() && ! class_exists( 'TMDO_Woocommerce_Bootstrap' ) ) { $missing['WooCommerce'] = '2meet-data-optimizer-woocommerce-addon'; } + // 2meet-hub-core / 2meet-spoke-sso read and write the user `sso` group it owns. + if ( ( class_exists( 'TMHC_Plugin', false ) || class_exists( 'TMSO_Plugin', false ) ) && ! class_exists( 'TMDO_SSO_Integration', false ) ) { + $missing['2meet Hub / Spoke SSO'] = '2meet-data-optimizer-sso-addon'; + } return $missing; } diff --git a/includes/integrations/class-tmdo-member-fields.php b/includes/integrations/class-tmdo-member-fields.php index 776d9cd..5cd847e 100644 --- a/includes/integrations/class-tmdo-member-fields.php +++ b/includes/integrations/class-tmdo-member-fields.php @@ -15,13 +15,13 @@ if ( ! defined( 'ABSPATH' ) ) { } /** - * Registers four user entity groups for membership, activity, profile, and SSO. + * Registers the user entity groups for membership, activity and profile. * * Groups → flat tables: * membership → wp_wpdo_user_membership (tier, points, expiry — high-freq search) * activity → wp_wpdo_user_activity (login counters, last-active — high-freq write) * profile → wp_wpdo_user_profile (display fields, specialties — low-freq write) - * sso → wp_wpdo_user_sso (Hub token cache, replaces _tmso_* usermeta) + * (sso → wp_wpdo_user_sso is owned by 2meet-data-optimizer-sso-addon) * * Called from TMDO_Core::run() before wpdo_register_entity_fields fires. * Pattern mirrors TMDO_WooCommerce::register() / register_user_entity_fields(). @@ -61,7 +61,6 @@ final class TMDO_Member_Fields { self::register_membership_group(); self::register_activity_group(); self::register_profile_group(); - self::register_sso_group(); self::register_core_profile_group(); self::register_social_group(); self::register_commerce_group(); @@ -213,70 +212,6 @@ final class TMDO_Member_Fields { ); } - /** - * Hub/Spoke SSO token cache — replaces _tmso_* usermeta. - * - * Silent refresh fires every 15 min per user; at 10M users this is a - * high-frequency EAV hot-spot. A flat table + object cache hit cuts DB - * load 10–50× versus a wp_usermeta EAV scan per refresh. - * - * last_id_token is NOT stored in plaintext (privacy + volume). Only the - * SHA-256 hash is kept for SLO token comparison. - * - * @return void - */ - private static function register_sso_group(): void { - TMDO_Entity_Registry::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', - ), - array( - 'key' => 'known_login_ips', - 'type' => 'textarea', - 'label' => 'Recent login IPs JSON array (new-device notification) — replaces _tmso_known_ips usermeta', - ), - ) - ); - } - /** * WP core user fields stored as multi-row EAV in wp_usermeta. * diff --git a/tests/unit/DeprecatedAddonsNoticeTest.php b/tests/unit/DeprecatedAddonsNoticeTest.php index fe2e5fe..c858e90 100644 --- a/tests/unit/DeprecatedAddonsNoticeTest.php +++ b/tests/unit/DeprecatedAddonsNoticeTest.php @@ -43,6 +43,17 @@ class DeprecatedAddonsNoticeTest extends TestCase { $this->assertStringContainsString( 'PARTNER-REGISTRATION.md', $html ); } + public function test_hub_and_spoke_point_to_the_sso_addon(): void { + $GLOBALS['_wp_options']['active_plugins'] = array( '2meet-data-optimizer-spoke-addon/2meet-data-optimizer-spoke-addon.php' ); + + ob_start(); + TMDO_Deprecated_Addons_Notice::render(); + $html = (string) ob_get_clean(); + + $this->assertStringContainsString( '2meet-data-optimizer-spoke-addon', $html ); + $this->assertStringContainsString( '2meet-data-optimizer-sso-addon', $html ); + } + public function test_no_notice_when_none_active_or_user_cannot_manage(): void { ob_start(); TMDO_Deprecated_Addons_Notice::render(); diff --git a/tests/unit/MemberFields/MemberFieldsRegistrationTest.php b/tests/unit/MemberFields/MemberFieldsRegistrationTest.php index f3dcaea..0e871fa 100644 --- a/tests/unit/MemberFields/MemberFieldsRegistrationTest.php +++ b/tests/unit/MemberFields/MemberFieldsRegistrationTest.php @@ -39,12 +39,18 @@ class MemberFieldsRegistrationTest extends TestCase { public function test_all_four_groups_are_registered(): void { WPDO_Member_Fields::register_entity_fields(); - foreach ( array( 'membership', 'activity', 'profile', 'sso' ) as $group ) { + foreach ( array( 'membership', 'activity', 'profile' ) as $group ) { $fields = WPDO_Entity_Registry::get_group_fields( 'user', $group ); $this->assertNotEmpty( $fields, "Group '{$group}' should have registered fields." ); } } + public function test_core_leaves_the_sso_group_to_the_sso_addon(): void { + WPDO_Member_Fields::register_entity_fields(); + + $this->assertSame( array(), WPDO_Entity_Registry::get_group_fields( 'user', 'sso' ) ); + } + public function test_core_registers_no_hp_user_group(): void { WPDO_Member_Fields::register_entity_fields(); @@ -148,41 +154,6 @@ class MemberFieldsRegistrationTest extends TestCase { $this->assertTrue( (bool) $field['fulltext'] ); } - // ── sso group ──────────────────────────────────────────────────────────── - - public function test_sso_group_has_eight_fields(): void { - WPDO_Member_Fields::register_entity_fields(); - $fields = WPDO_Entity_Registry::get_group_fields( 'user', 'sso' ); - // v3.4.4: known_login_ips registered (was leaking to wp_usermeta via set_entity). - $this->assertCount( 8, $fields ); - $this->assertNotNull( $this->find_field( $fields, 'known_login_ips' ) ); - } - - public function test_hub_global_user_id_is_searchable(): void { - WPDO_Member_Fields::register_entity_fields(); - $fields = WPDO_Entity_Registry::get_group_fields( 'user', 'sso' ); - $field = $this->find_field( $fields, 'hub_global_user_id' ); - - $this->assertTrue( (bool) $field['searchable'] ); - } - - public function test_token_expires_at_is_datetime_searchable(): void { - WPDO_Member_Fields::register_entity_fields(); - $fields = WPDO_Entity_Registry::get_group_fields( 'user', 'sso' ); - $field = $this->find_field( $fields, 'token_expires_at' ); - - $this->assertSame( 'datetime', $field['type'] ); - $this->assertTrue( (bool) $field['searchable'] ); - } - - public function test_refresh_token_enc_is_textarea(): void { - WPDO_Member_Fields::register_entity_fields(); - $fields = WPDO_Entity_Registry::get_group_fields( 'user', 'sso' ); - $field = $this->find_field( $fields, 'refresh_token_enc' ); - - $this->assertSame( 'textarea', $field['type'] ); - } - // ── register() hooks add_action ────────────────────────────────────────── public function test_register_hooks_wpdo_register_entity_fields(): void { diff --git a/tests/unit/MissingIntegrationAddonsTest.php b/tests/unit/MissingIntegrationAddonsTest.php index 9237221..ae93ead 100644 --- a/tests/unit/MissingIntegrationAddonsTest.php +++ b/tests/unit/MissingIntegrationAddonsTest.php @@ -79,6 +79,27 @@ class MissingIntegrationAddonsTest extends TestCase { $this->assertSame( array( 'WooCommerce' => '2meet-data-optimizer-woocommerce-addon' ), TMDO_Compatibility::missing_integration_addons() ); } + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_hub_or_spoke_without_the_sso_addon_is_reported(): void { + eval( 'final class TMSO_Plugin {}' ); + + $this->assertSame( array( '2meet Hub / Spoke SSO' => '2meet-data-optimizer-sso-addon' ), TMDO_Compatibility::missing_integration_addons() ); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_hub_with_the_sso_addon_is_fine(): void { + eval( 'final class TMHC_Plugin {}' ); + eval( 'final class TMDO_SSO_Integration {}' ); + + $this->assertSame( array(), TMDO_Compatibility::missing_integration_addons() ); + } + /** * @runInSeparateProcess * @preserveGlobalState disabled