PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.10.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.10.0
2.11.0 2.10.0 2.9.0 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 All 52 releases
← All changes | includes/abilities/class-abilities-registrar.php +225 -10 1.26.0 → 2.10.0 View file →
@@ -8,13 +8,21 @@
8 8 declare(strict_types=1);
9 9
10 10 namespace ThinkRank\Abilities;
11 11
12 +if ( ! defined( 'ABSPATH' ) ) {
13 + exit; // Exit if accessed directly.
14 +}
15 +
12 16 use ThinkRank\Abilities\Analysis\Get_Post_Seo_Checks;
13 17 use ThinkRank\Abilities\Analysis\Get_Term_Seo_Checks;
14 18 use ThinkRank\Abilities\Content\Get_Post_Seo;
15 19 use ThinkRank\Abilities\Content\Get_Term_Seo;
16 20 use ThinkRank\Abilities\Content\List_Content_Items;
21 +use ThinkRank\Abilities\Content\Get_Duplicate_Snippets;
22 +use ThinkRank\Abilities\Content\Get_Thin_Content;
23 +use ThinkRank\Abilities\Content\List_Snippet_Issues;
24 +use ThinkRank\Abilities\Content\Update_Thin_Content_Settings;
17 25 use ThinkRank\Abilities\Content\List_Content_Types;
18 26 use ThinkRank\Abilities\Content\Update_Post_Seo;
19 27 use ThinkRank\Abilities\Content\Update_Term_Seo;
20 28 use ThinkRank\Abilities\Content\Generate_Content_Brief;
@@ -40,10 +48,22 @@
40 48 use ThinkRank\Abilities\Settings\Update_Image_Seo_Settings;
41 49 use ThinkRank\Abilities\Settings\Get_Llms_Txt_Settings;
42 50 use ThinkRank\Abilities\Settings\Update_Llms_Txt_Settings;
43 51 use ThinkRank\Abilities\Settings\Get_Robots_Meta_Settings;
52 +use ThinkRank\Abilities\Links\Get_External_Links_Settings;
53 +use ThinkRank\Abilities\Links\Update_External_Links_Settings;
54 +use ThinkRank\Abilities\Matrix\Get_Content_Type_Matrix;
55 +use ThinkRank\Abilities\Matrix\Update_Content_Type_Matrix;
56 +use ThinkRank\Abilities\Media\List_Images;
57 +use ThinkRank\Abilities\Media\Get_Image_Alt_Text;
58 +use ThinkRank\Abilities\Media\Update_Image_Alt_Text;
59 +use ThinkRank\Abilities\Media\Fill_Missing_Alt_Text;
60 +use ThinkRank\Abilities\Content\Get_Post_Content;
61 +use ThinkRank\Abilities\Maintenance\Purge_Caches;
44 62 use ThinkRank\Abilities\Settings\Update_Robots_Meta_Settings;
45 63 use ThinkRank\Abilities\Settings\Get_Instant_Indexing_Settings;
64 +use ThinkRank\Abilities\Settings\Get_Ai_Budget;
65 +use ThinkRank\Abilities\Settings\Update_Ai_Budget;
46 66 use ThinkRank\Abilities\Settings\Update_Instant_Indexing_Settings;
47 67 use ThinkRank\Abilities\Settings\Get_Author_Archives_Settings;
48 68 use ThinkRank\Abilities\Settings\Update_Author_Archives_Settings;
49 69 use ThinkRank\Abilities\Settings\Get_Email_Report_Settings;
@@ -56,9 +76,8 @@
56 76 use ThinkRank\Abilities\Analysis\Get_Llms_Txt_Status;
57 77 use ThinkRank\Abilities\Analysis\Generate_Llms_Txt;
58 78 use ThinkRank\Abilities\Analysis\Publish_Llms_Txt;
59 79 use ThinkRank\Abilities\Analysis\Get_Seo_Analytics_Data;
60 -use ThinkRank\Abilities\Analysis\Get_Seo_Insights;
61 80 use ThinkRank\Abilities\Analysis\Get_Seo_Opportunities;
62 81 use ThinkRank\Abilities\Analysis\Get_Seo_Score;
63 82 use ThinkRank\Abilities\Analysis\Get_Seo_Analyzer;
64 83 use ThinkRank\Abilities\Analysis\Run_Seo_Analyzer;
@@ -67,12 +86,8 @@
67 86 use ThinkRank\Abilities\Analysis\Get_Connection_Status;
68 87 use ThinkRank\Abilities\Analysis\Bulk_Analyze_And_Save;
69 88 use ThinkRank\Core\Settings;
70 89
71 -if ( ! defined( 'ABSPATH' ) ) {
72 - exit; // Exit if accessed directly.
73 -}
74 -
75 90 /**
76 91 * Registers ThinkRank abilities with the WordPress Abilities API.
77 92 *
78 93 * Abilities are **always** registered when the Abilities API is available —
@@ -92,22 +107,207 @@
92 107 */
93 108 class Abilities_Registrar {
94 109
95 110 /**
111 + * Ability-name prefixes that mark an ability as ThinkRank's. The free
112 + * plugin owns `thinkrank/`; Pro registers under `thinkrank-pro/` via the
113 + * `thinkrank_register_abilities` filter.
114 + */
115 + public const ABILITY_PREFIXES = [ 'thinkrank/', 'thinkrank-pro/' ];
116 +
117 + /**
118 + * Whether the registration replay (see {@see self::ensure_registered()})
119 + * has already run this request. One attempt only — a replay that produced
120 + * nothing will not produce anything on the second try either, and the MCP
121 + * server asks for the tool list more than once per request.
122 + *
123 + * @var bool
124 + */
125 + private static $replayed = false;
126 +
127 + /**
96 128 * Initialize the registrar (called by the plugin's component container).
97 129 *
130 + * Deliberately does NOT bail on a missing `wp_register_ability`: the
131 + * Abilities API is a set of GLOBAL functions loaded under
132 + * `function_exists` guards, so which copy owns them — ours in
133 + * `dependencies/`, another plugin's, or core's — is decided by load order,
134 + * not by us. A copy that lands after `plugins_loaded` would have made this
135 + * an early return and left ThinkRank permanently unregistered (see #241).
136 + * The callbacks themselves are guarded instead, so hooking unconditionally
137 + * is free when no Abilities API ever shows up.
138 + *
98 139 * @return void
99 140 */
100 141 public function init() {
101 - if ( ! function_exists( 'wp_register_ability' ) ) {
102 - return;
142 + add_action( 'wp_abilities_api_categories_init', [ $this, 'register_category' ] );
143 + add_action( 'wp_abilities_api_init', [ $this, 'register_abilities' ] );
144 + }
145 +
146 + /**
147 + * Guarantee ThinkRank's abilities are in the registry, replaying
148 + * registration once if they are not.
149 + *
150 + * `wp_abilities_api_init` fires exactly once, from the lazy registry
151 + * singleton of whichever Abilities API copy owns the globals. If a foreign
152 + * copy owns them and fires its init under a different name or at a moment
153 + * when our hook is not attached yet, our callback never runs: the registry
154 + * is populated by everyone else and `tools/list` answers with an empty
155 + * array while auth, discovery and `initialize` all report success (#241).
156 + *
157 + * Calling `wp_get_abilities()` here forces that lazy init, so by the time
158 + * we decide to replay, `wp_abilities_api_init` has fired and
159 + * `wp_register_ability()` will accept our registrations. Registration is
160 + * idempotent (each ability is skipped when `wp_has_ability()` already
161 + * knows it), so a replay after a *successful* hook run is a no-op anyway.
162 + *
163 + * @param callable|null $replay Optional replay routine, for tests. Default
164 + * is this class's own category + abilities
165 + * registration.
166 + * @return int Number of ThinkRank abilities registered afterwards.
167 + */
168 + public static function ensure_registered( ?callable $replay = null ): int {
169 + if ( ! function_exists( 'wp_get_abilities' ) ) {
170 + return 0;
103 171 }
104 172
105 - add_action( 'wp_abilities_api_categories_init', [ $this, 'register_category' ] );
106 - add_action( 'wp_abilities_api_init', [ $this, 'register_abilities' ] );
173 + // Forces the registry's lazy init (and with it `wp_abilities_api_init`).
174 + $count = self::count_registered();
175 + if ( $count > 0 || self::$replayed ) {
176 + return $count;
177 + }
178 +
179 + // Before the registry has initialized, `wp_register_ability()` refuses
180 + // the registration and calls `_doing_it_wrong()`. Nothing to replay yet.
181 + if ( ! function_exists( 'did_action' ) || ! did_action( 'wp_abilities_api_init' ) ) {
182 + return $count;
183 + }
184 +
185 + self::$replayed = true;
186 +
187 + if ( ! Ability_Base::abilities_enabled() ) {
188 + return 0;
189 + }
190 +
191 + if ( null === $replay ) {
192 + $registrar = new self();
193 + $replay = static function () use ( $registrar ) {
194 + $registrar->register_category();
195 + $registrar->register_abilities();
196 + };
197 + }
198 +
199 + $replay();
200 +
201 + $count = self::count_registered();
202 +
203 + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
204 + error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- WP_DEBUG-gated diagnostic.
205 + '[TR-MCP] ThinkRank abilities were missing from the registry; replayed registration. ' . self::summary()
206 + );
207 + }
208 +
209 + return $count;
107 210 }
108 211
109 212 /**
213 + * How many ThinkRank abilities the registry currently holds.
214 + *
215 + * @return int
216 + */
217 + public static function count_registered(): int {
218 + if ( ! function_exists( 'wp_get_abilities' ) ) {
219 + return 0;
220 + }
221 +
222 + $count = 0;
223 + foreach ( wp_get_abilities() as $ability ) {
224 + if ( ! is_object( $ability ) || ! method_exists( $ability, 'get_name' ) ) {
225 + continue;
226 + }
227 + foreach ( self::ABILITY_PREFIXES as $prefix ) {
228 + if ( 0 === strpos( (string) $ability->get_name(), $prefix ) ) {
229 + ++$count;
230 + break;
231 + }
232 + }
233 + }
234 +
235 + return $count;
236 + }
237 +
238 + /**
239 + * Which file defines the global Abilities API functions for this request.
240 + * A path outside ThinkRank's `dependencies/` means a foreign copy owns the
241 + * registry — the precondition for #241.
242 + *
243 + * @return string Absolute path, or '' when the API is absent/unresolvable.
244 + */
245 + public static function owner_path(): string {
246 + if ( ! function_exists( 'wp_get_abilities' ) ) {
247 + return '';
248 + }
249 +
250 + try {
251 + $reflection = new \ReflectionFunction( 'wp_get_abilities' );
252 + return (string) $reflection->getFileName();
253 + } catch ( \ReflectionException $e ) {
254 + return '';
255 + }
256 + }
257 +
258 + /**
259 + * Diagnostic snapshot of the Abilities API as this request sees it. Feeds
260 + * the MCP self-test and the debug log so "no tools registered" is
261 + * distinguishable from "tools filtered out" without shell access.
262 + *
263 + * @return array{api_available:bool, owner:string, foreign:bool, hook_fired:bool, total:int, thinkrank:int, replayed:bool}
264 + */
265 + public static function diagnostics(): array {
266 + $available = function_exists( 'wp_get_abilities' );
267 + $owner = self::owner_path();
268 + $bundled = defined( 'THINKRANK_PLUGIN_DIR' ) ? THINKRANK_PLUGIN_DIR : '';
269 +
270 + // Read the registry BEFORE `hook_fired`: `wp_get_abilities()` forces the
271 + // lazy singleton's init (which fires `wp_abilities_api_init`). Reading
272 + // `did_action()` first would report `hook_fired => false` in the same
273 + // snapshot that already counts registered abilities — an internally
274 + // inconsistent line for the exact support scenario this feeds.
275 + $total = $available ? count( wp_get_abilities() ) : 0;
276 + $thinkrank = self::count_registered();
277 +
278 + return [
279 + 'api_available' => $available,
280 + 'owner' => $owner,
281 + 'foreign' => ( '' !== $owner && '' !== $bundled && 0 !== strpos( $owner, $bundled ) ),
282 + 'hook_fired' => function_exists( 'did_action' ) ? (bool) did_action( 'wp_abilities_api_init' ) : false,
283 + 'total' => $total,
284 + 'thinkrank' => $thinkrank,
285 + 'replayed' => self::$replayed,
286 + ];
287 + }
288 +
289 + /**
290 + * One-line, human-readable form of {@see self::diagnostics()}.
291 + *
292 + * @return string
293 + */
294 + public static function summary(): string {
295 + $d = self::diagnostics();
296 +
297 + return sprintf(
298 + 'Abilities API: %s; owner: %s%s; abilities total: %d, thinkrank: %d; init fired: %s; replayed: %s',
299 + $d['api_available'] ? 'present' : 'missing',
300 + '' !== $d['owner'] ? $d['owner'] : 'unknown',
301 + $d['foreign'] ? ' (foreign copy — not ThinkRank\'s bundled runtime)' : '',
302 + $d['total'],
303 + $d['thinkrank'],
304 + $d['hook_fired'] ? 'yes' : 'no',
305 + $d['replayed'] ? 'yes' : 'no'
306 + );
307 + }
308 +
309 + /**
110 310 * Register the ThinkRank ability category.
111 311 *
112 312 * @return void
113 313 */
@@ -143,8 +343,12 @@
143 343 new Get_Global_Settings(),
144 344 new Update_Global_Settings(),
145 345 new Get_Post_Seo(),
146 346 new Update_Post_Seo(),
347 + new List_Snippet_Issues(),
348 + new Get_Duplicate_Snippets(),
349 + new Get_Thin_Content(),
350 + new Update_Thin_Content_Settings(),
147 351 new Get_Term_Seo(),
148 352 new Update_Term_Seo(),
149 353 new Get_Post_Seo_Checks(),
150 354 new Get_Term_Seo_Checks(),
@@ -162,9 +366,21 @@
162 366 new Update_Image_Seo_Settings(),
163 367 new Get_Llms_Txt_Settings(),
164 368 new Update_Llms_Txt_Settings(),
165 369 new Get_Robots_Meta_Settings(),
370 + new Get_External_Links_Settings(),
371 + new Update_External_Links_Settings(),
372 + new Get_Content_Type_Matrix(),
373 + new Update_Content_Type_Matrix(),
374 + new List_Images(),
375 + new Get_Image_Alt_Text(),
376 + new Update_Image_Alt_Text(),
377 + new Fill_Missing_Alt_Text(),
378 + new Get_Post_Content(),
379 + new Purge_Caches(),
166 380 new Update_Robots_Meta_Settings(),
381 + new Get_Ai_Budget(),
382 + new Update_Ai_Budget(),
167 383 new Get_Instant_Indexing_Settings(),
168 384 new Update_Instant_Indexing_Settings(),
169 385 new Get_Author_Archives_Settings(),
170 386 new Update_Author_Archives_Settings(),
@@ -178,9 +394,8 @@
178 394 new Get_Llms_Txt_Status(),
179 395 new Generate_Llms_Txt(),
180 396 new Publish_Llms_Txt(),
181 397 new Get_Seo_Analytics_Data(),
182 - new Get_Seo_Insights(),
183 398 new Get_Seo_Opportunities(),
184 399 new Get_Seo_Score(),
185 400 new Get_Seo_Analyzer(),
186 401 new Run_Seo_Analyzer(),