0 || self::$replayed ) { return $count; } // Before the registry has initialized, `wp_register_ability()` refuses // the registration and calls `_doing_it_wrong()`. Nothing to replay yet. if ( ! function_exists( 'did_action' ) || ! did_action( 'wp_abilities_api_init' ) ) { return $count; } self::$replayed = true; if ( ! Ability_Base::abilities_enabled() ) { return 0; } if ( null === $replay ) { $registrar = new self(); $replay = static function () use ( $registrar ) { $registrar->register_category(); $registrar->register_abilities(); }; } $replay(); $count = self::count_registered(); if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- WP_DEBUG-gated diagnostic. '[TR-MCP] ThinkRank abilities were missing from the registry; replayed registration. ' . self::summary() ); } return $count; } /** * How many ThinkRank abilities the registry currently holds. * * @return int */ public static function count_registered(): int { if ( ! function_exists( 'wp_get_abilities' ) ) { return 0; } $count = 0; foreach ( wp_get_abilities() as $ability ) { if ( ! is_object( $ability ) || ! method_exists( $ability, 'get_name' ) ) { continue; } foreach ( self::ABILITY_PREFIXES as $prefix ) { if ( 0 === strpos( (string) $ability->get_name(), $prefix ) ) { ++$count; break; } } } return $count; } /** * Which file defines the global Abilities API functions for this request. * A path outside ThinkRank's `dependencies/` means a foreign copy owns the * registry — the precondition for #241. * * @return string Absolute path, or '' when the API is absent/unresolvable. */ public static function owner_path(): string { if ( ! function_exists( 'wp_get_abilities' ) ) { return ''; } try { $reflection = new \ReflectionFunction( 'wp_get_abilities' ); return (string) $reflection->getFileName(); } catch ( \ReflectionException $e ) { return ''; } } /** * Diagnostic snapshot of the Abilities API as this request sees it. Feeds * the MCP self-test and the debug log so "no tools registered" is * distinguishable from "tools filtered out" without shell access. * * @return array{api_available:bool, owner:string, foreign:bool, hook_fired:bool, total:int, thinkrank:int, replayed:bool} */ public static function diagnostics(): array { $available = function_exists( 'wp_get_abilities' ); $owner = self::owner_path(); $bundled = defined( 'THINKRANK_PLUGIN_DIR' ) ? THINKRANK_PLUGIN_DIR : ''; // Read the registry BEFORE `hook_fired`: `wp_get_abilities()` forces the // lazy singleton's init (which fires `wp_abilities_api_init`). Reading // `did_action()` first would report `hook_fired => false` in the same // snapshot that already counts registered abilities — an internally // inconsistent line for the exact support scenario this feeds. $total = $available ? count( wp_get_abilities() ) : 0; $thinkrank = self::count_registered(); return [ 'api_available' => $available, 'owner' => $owner, 'foreign' => ( '' !== $owner && '' !== $bundled && 0 !== strpos( $owner, $bundled ) ), 'hook_fired' => function_exists( 'did_action' ) ? (bool) did_action( 'wp_abilities_api_init' ) : false, 'total' => $total, 'thinkrank' => $thinkrank, 'replayed' => self::$replayed, ]; } /** * One-line, human-readable form of {@see self::diagnostics()}. * * @return string */ public static function summary(): string { $d = self::diagnostics(); return sprintf( 'Abilities API: %s; owner: %s%s; abilities total: %d, thinkrank: %d; init fired: %s; replayed: %s', $d['api_available'] ? 'present' : 'missing', '' !== $d['owner'] ? $d['owner'] : 'unknown', $d['foreign'] ? ' (foreign copy — not ThinkRank\'s bundled runtime)' : '', $d['total'], $d['thinkrank'], $d['hook_fired'] ? 'yes' : 'no', $d['replayed'] ? 'yes' : 'no' ); } /** * Register the ThinkRank ability category. * * @return void */ public function register_category() { if ( function_exists( 'wp_has_ability_category' ) && wp_has_ability_category( 'thinkrank' ) ) { return; } if ( function_exists( 'wp_register_ability_category' ) ) { wp_register_ability_category( 'thinkrank', [ 'label' => __( 'ThinkRank', 'thinkrank' ), 'description' => __( 'SEO settings, metadata, and analysis abilities powered by ThinkRank.', 'thinkrank' ), ] ); } } /** * Register ThinkRank abilities. * * @return void */ public function register_abilities() { if ( ! Ability_Base::abilities_enabled() ) { return; } $abilities = [ new List_Content_Types(), new List_Content_Items(), new Get_Global_Settings(), new Update_Global_Settings(), new Get_Post_Seo(), new Update_Post_Seo(), new List_Snippet_Issues(), new Get_Term_Seo(), new Update_Term_Seo(), new Get_Post_Seo_Checks(), new Get_Term_Seo_Checks(), new Get_Robots_Txt(), new Update_Robots_Txt(), new Get_Sitemap_Settings(), new Update_Sitemap_Settings(), new Get_Schema_Settings(), new Update_Schema_Settings(), new Get_Site_Identity_Settings(), new Update_Site_Identity_Settings(), new Get_Social_Meta_Settings(), new Update_Social_Meta_Settings(), new Get_Image_Seo_Settings(), new Update_Image_Seo_Settings(), new Get_Llms_Txt_Settings(), new Update_Llms_Txt_Settings(), new Get_Robots_Meta_Settings(), new Get_External_Links_Settings(), new Update_External_Links_Settings(), new Get_Content_Type_Matrix(), new Update_Content_Type_Matrix(), new List_Images(), new Get_Image_Alt_Text(), new Update_Image_Alt_Text(), new Fill_Missing_Alt_Text(), new Get_Post_Content(), new Purge_Caches(), new Update_Robots_Meta_Settings(), new Get_Ai_Budget(), new Update_Ai_Budget(), new Get_Instant_Indexing_Settings(), new Update_Instant_Indexing_Settings(), new Get_Author_Archives_Settings(), new Update_Author_Archives_Settings(), new Get_Email_Report_Settings(), new Update_Email_Report_Settings(), new Send_Email_Report_Test(), new Get_Social_Platforms_Settings(), new Update_Social_Platforms_Settings(), new Submit_Urls_To_Index(), new Get_Instant_Indexing_History(), new Get_Llms_Txt_Status(), new Generate_Llms_Txt(), new Publish_Llms_Txt(), new Get_Seo_Analytics_Data(), new Get_Seo_Opportunities(), new Get_Seo_Score(), new Get_Seo_Analyzer(), new Run_Seo_Analyzer(), new Get_Performance_Data(), new Get_Integrations_Status(), new Get_Connection_Status(), new Bulk_Analyze_And_Save(), new Generate_Content_Brief(), new Get_Pillar_Content(), new Update_Pillar_Content(), new Detect_Import_Sources(), new Get_Import_Status(), new Run_Seo_Import(), new Preview_Seo_Import(), ]; $abilities = apply_filters( 'thinkrank_register_abilities', $abilities ); foreach ( $abilities as $ability ) { if ( ! $ability instanceof Ability_Base ) { continue; } if ( ! $ability->meets_capability_policy() || ! $ability->is_enabled() ) { continue; } if ( function_exists( 'wp_has_ability' ) && wp_has_ability( $ability->get_id() ) ) { continue; } $ability->register(); } } }