PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.11.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.11.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
thinkrank / includes / abilities / class-abilities-registrar.php

class-abilities-registrar.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.11.0, at includes/abilities/class-abilities-registrar.php

438 lines 15.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Abilities registrar.
4 *
5 * @package ThinkRank\Abilities
6 */
7
8 declare(strict_types=1);
9
10 namespace ThinkRank\Abilities;
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit; // Exit if accessed directly.
14 }
15
16 use ThinkRank\Abilities\Analysis\Get_Post_Seo_Checks;
17 use ThinkRank\Abilities\Analysis\Get_Term_Seo_Checks;
18 use ThinkRank\Abilities\Content\Get_Post_Seo;
19 use ThinkRank\Abilities\Content\Get_Term_Seo;
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;
25 use ThinkRank\Abilities\Content\List_Content_Types;
26 use ThinkRank\Abilities\Content\Update_Post_Seo;
27 use ThinkRank\Abilities\Content\Update_Term_Seo;
28 use ThinkRank\Abilities\Content\Generate_Content_Brief;
29 use ThinkRank\Abilities\Content\Get_Pillar_Content;
30 use ThinkRank\Abilities\Content\Update_Pillar_Content;
31 use ThinkRank\Abilities\Import\Detect_Import_Sources;
32 use ThinkRank\Abilities\Import\Get_Import_Status;
33 use ThinkRank\Abilities\Import\Run_Seo_Import;
34 use ThinkRank\Abilities\Import\Preview_Seo_Import;
35 use ThinkRank\Abilities\Settings\Get_Global_Settings;
36 use ThinkRank\Abilities\Settings\Get_Robots_Txt;
37 use ThinkRank\Abilities\Settings\Get_Sitemap_Settings;
38 use ThinkRank\Abilities\Settings\Update_Global_Settings;
39 use ThinkRank\Abilities\Settings\Update_Robots_Txt;
40 use ThinkRank\Abilities\Settings\Update_Sitemap_Settings;
41 use ThinkRank\Abilities\Settings\Get_Schema_Settings;
42 use ThinkRank\Abilities\Settings\Update_Schema_Settings;
43 use ThinkRank\Abilities\Settings\Get_Site_Identity_Settings;
44 use ThinkRank\Abilities\Settings\Update_Site_Identity_Settings;
45 use ThinkRank\Abilities\Settings\Get_Social_Meta_Settings;
46 use ThinkRank\Abilities\Settings\Update_Social_Meta_Settings;
47 use ThinkRank\Abilities\Settings\Get_Image_Seo_Settings;
48 use ThinkRank\Abilities\Settings\Update_Image_Seo_Settings;
49 use ThinkRank\Abilities\Settings\Get_Llms_Txt_Settings;
50 use ThinkRank\Abilities\Settings\Update_Llms_Txt_Settings;
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\Content\Get_FAQ;
57 use ThinkRank\Abilities\Content\Update_FAQ;
58 use ThinkRank\Abilities\Media\List_Images;
59 use ThinkRank\Abilities\Media\Get_Image_Alt_Text;
60 use ThinkRank\Abilities\Media\Update_Image_Alt_Text;
61 use ThinkRank\Abilities\Media\Fill_Missing_Alt_Text;
62 use ThinkRank\Abilities\Content\Get_Post_Content;
63 use ThinkRank\Abilities\Maintenance\Purge_Caches;
64 use ThinkRank\Abilities\Settings\Update_Robots_Meta_Settings;
65 use ThinkRank\Abilities\Settings\Get_Instant_Indexing_Settings;
66 use ThinkRank\Abilities\Settings\Get_Ai_Budget;
67 use ThinkRank\Abilities\Settings\Update_Ai_Budget;
68 use ThinkRank\Abilities\Settings\Update_Instant_Indexing_Settings;
69 use ThinkRank\Abilities\Settings\Get_Author_Archives_Settings;
70 use ThinkRank\Abilities\Settings\Update_Author_Archives_Settings;
71 use ThinkRank\Abilities\Settings\Get_Email_Report_Settings;
72 use ThinkRank\Abilities\Settings\Update_Email_Report_Settings;
73 use ThinkRank\Abilities\Settings\Send_Email_Report_Test;
74 use ThinkRank\Abilities\Settings\Get_Social_Platforms_Settings;
75 use ThinkRank\Abilities\Settings\Update_Social_Platforms_Settings;
76 use ThinkRank\Abilities\Indexing\Submit_Urls_To_Index;
77 use ThinkRank\Abilities\Indexing\Get_Instant_Indexing_History;
78 use ThinkRank\Abilities\Analysis\Get_Llms_Txt_Status;
79 use ThinkRank\Abilities\Analysis\Generate_Llms_Txt;
80 use ThinkRank\Abilities\Analysis\Publish_Llms_Txt;
81 use ThinkRank\Abilities\Analysis\Get_Seo_Analytics_Data;
82 use ThinkRank\Abilities\Analysis\Get_Seo_Opportunities;
83 use ThinkRank\Abilities\Analysis\Get_Seo_Score;
84 use ThinkRank\Abilities\Analysis\Get_Seo_Analyzer;
85 use ThinkRank\Abilities\Analysis\Run_Seo_Analyzer;
86 use ThinkRank\Abilities\Analysis\Get_Performance_Data;
87 use ThinkRank\Abilities\Analysis\Get_Integrations_Status;
88 use ThinkRank\Abilities\Analysis\Get_Connection_Status;
89 use ThinkRank\Abilities\Analysis\Bulk_Analyze_And_Save;
90 use ThinkRank\Core\Settings;
91
92 /**
93 * Registers ThinkRank abilities with the WordPress Abilities API.
94 *
95 * Abilities are **always** registered when the Abilities API is available —
96 * registration is decoupled from the `enable_mcp` toggle (see #187). Each
97 * ability is a permission-checked read/write surface (its own
98 * `current_user_can()` callback runs on every call), so registration alone
99 * exposes nothing; it only makes ThinkRank discoverable to generic WordPress
100 * Abilities clients (e.g. an external connector) the same way WordPress core
101 * abilities are. The `enable_mcp` toggle gates only ThinkRank's own MCP server
102 * ({@see \ThinkRank\Mcp\Mcp_Manager}) — the endpoint, discovery documents, and
103 * OAuth surface. The MCP server reads this abilities registry as its tool
104 * catalog. The Abilities API itself ships in `dependencies/` and no-ops
105 * gracefully when missing.
106 *
107 * A kill switch remains available via the `thinkrank_abilities_api_enabled`
108 * filter (defaults to `true`; see {@see Ability_Base::abilities_enabled()}).
109 */
110 class Abilities_Registrar {
111
112 /**
113 * Ability-name prefixes that mark an ability as ThinkRank's. The free
114 * plugin owns `thinkrank/`; Pro registers under `thinkrank-pro/` via the
115 * `thinkrank_register_abilities` filter.
116 */
117 public const ABILITY_PREFIXES = [ 'thinkrank/', 'thinkrank-pro/' ];
118
119 /**
120 * Whether the registration replay (see {@see self::ensure_registered()})
121 * has already run this request. One attempt only — a replay that produced
122 * nothing will not produce anything on the second try either, and the MCP
123 * server asks for the tool list more than once per request.
124 *
125 * @var bool
126 */
127 private static $replayed = false;
128
129 /**
130 * Initialize the registrar (called by the plugin's component container).
131 *
132 * Deliberately does NOT bail on a missing `wp_register_ability`: the
133 * Abilities API is a set of GLOBAL functions loaded under
134 * `function_exists` guards, so which copy owns them — ours in
135 * `dependencies/`, another plugin's, or core's — is decided by load order,
136 * not by us. A copy that lands after `plugins_loaded` would have made this
137 * an early return and left ThinkRank permanently unregistered (see #241).
138 * The callbacks themselves are guarded instead, so hooking unconditionally
139 * is free when no Abilities API ever shows up.
140 *
141 * @return void
142 */
143 public function init() {
144 add_action( 'wp_abilities_api_categories_init', [ $this, 'register_category' ] );
145 add_action( 'wp_abilities_api_init', [ $this, 'register_abilities' ] );
146 }
147
148 /**
149 * Guarantee ThinkRank's abilities are in the registry, replaying
150 * registration once if they are not.
151 *
152 * `wp_abilities_api_init` fires exactly once, from the lazy registry
153 * singleton of whichever Abilities API copy owns the globals. If a foreign
154 * copy owns them and fires its init under a different name or at a moment
155 * when our hook is not attached yet, our callback never runs: the registry
156 * is populated by everyone else and `tools/list` answers with an empty
157 * array while auth, discovery and `initialize` all report success (#241).
158 *
159 * Calling `wp_get_abilities()` here forces that lazy init, so by the time
160 * we decide to replay, `wp_abilities_api_init` has fired and
161 * `wp_register_ability()` will accept our registrations. Registration is
162 * idempotent (each ability is skipped when `wp_has_ability()` already
163 * knows it), so a replay after a *successful* hook run is a no-op anyway.
164 *
165 * @param callable|null $replay Optional replay routine, for tests. Default
166 * is this class's own category + abilities
167 * registration.
168 * @return int Number of ThinkRank abilities registered afterwards.
169 */
170 public static function ensure_registered( ?callable $replay = null ): int {
171 if ( ! function_exists( 'wp_get_abilities' ) ) {
172 return 0;
173 }
174
175 // Forces the registry's lazy init (and with it `wp_abilities_api_init`).
176 $count = self::count_registered();
177 if ( $count > 0 || self::$replayed ) {
178 return $count;
179 }
180
181 // Before the registry has initialized, `wp_register_ability()` refuses
182 // the registration and calls `_doing_it_wrong()`. Nothing to replay yet.
183 if ( ! function_exists( 'did_action' ) || ! did_action( 'wp_abilities_api_init' ) ) {
184 return $count;
185 }
186
187 self::$replayed = true;
188
189 if ( ! Ability_Base::abilities_enabled() ) {
190 return 0;
191 }
192
193 if ( null === $replay ) {
194 $registrar = new self();
195 $replay = static function () use ( $registrar ) {
196 $registrar->register_category();
197 $registrar->register_abilities();
198 };
199 }
200
201 $replay();
202
203 $count = self::count_registered();
204
205 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
206 error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- WP_DEBUG-gated diagnostic.
207 '[TR-MCP] ThinkRank abilities were missing from the registry; replayed registration. ' . self::summary()
208 );
209 }
210
211 return $count;
212 }
213
214 /**
215 * How many ThinkRank abilities the registry currently holds.
216 *
217 * @return int
218 */
219 public static function count_registered(): int {
220 if ( ! function_exists( 'wp_get_abilities' ) ) {
221 return 0;
222 }
223
224 $count = 0;
225 foreach ( wp_get_abilities() as $ability ) {
226 if ( ! is_object( $ability ) || ! method_exists( $ability, 'get_name' ) ) {
227 continue;
228 }
229 foreach ( self::ABILITY_PREFIXES as $prefix ) {
230 if ( 0 === strpos( (string) $ability->get_name(), $prefix ) ) {
231 ++$count;
232 break;
233 }
234 }
235 }
236
237 return $count;
238 }
239
240 /**
241 * Which file defines the global Abilities API functions for this request.
242 * A path outside ThinkRank's `dependencies/` means a foreign copy owns the
243 * registry — the precondition for #241.
244 *
245 * @return string Absolute path, or '' when the API is absent/unresolvable.
246 */
247 public static function owner_path(): string {
248 if ( ! function_exists( 'wp_get_abilities' ) ) {
249 return '';
250 }
251
252 try {
253 $reflection = new \ReflectionFunction( 'wp_get_abilities' );
254 return (string) $reflection->getFileName();
255 } catch ( \ReflectionException $e ) {
256 return '';
257 }
258 }
259
260 /**
261 * Diagnostic snapshot of the Abilities API as this request sees it. Feeds
262 * the MCP self-test and the debug log so "no tools registered" is
263 * distinguishable from "tools filtered out" without shell access.
264 *
265 * @return array{api_available:bool, owner:string, foreign:bool, hook_fired:bool, total:int, thinkrank:int, replayed:bool}
266 */
267 public static function diagnostics(): array {
268 $available = function_exists( 'wp_get_abilities' );
269 $owner = self::owner_path();
270 $bundled = defined( 'THINKRANK_PLUGIN_DIR' ) ? THINKRANK_PLUGIN_DIR : '';
271
272 // Read the registry BEFORE `hook_fired`: `wp_get_abilities()` forces the
273 // lazy singleton's init (which fires `wp_abilities_api_init`). Reading
274 // `did_action()` first would report `hook_fired => false` in the same
275 // snapshot that already counts registered abilities — an internally
276 // inconsistent line for the exact support scenario this feeds.
277 $total = $available ? count( wp_get_abilities() ) : 0;
278 $thinkrank = self::count_registered();
279
280 return [
281 'api_available' => $available,
282 'owner' => $owner,
283 'foreign' => ( '' !== $owner && '' !== $bundled && 0 !== strpos( $owner, $bundled ) ),
284 'hook_fired' => function_exists( 'did_action' ) ? (bool) did_action( 'wp_abilities_api_init' ) : false,
285 'total' => $total,
286 'thinkrank' => $thinkrank,
287 'replayed' => self::$replayed,
288 ];
289 }
290
291 /**
292 * One-line, human-readable form of {@see self::diagnostics()}.
293 *
294 * @return string
295 */
296 public static function summary(): string {
297 $d = self::diagnostics();
298
299 return sprintf(
300 'Abilities API: %s; owner: %s%s; abilities total: %d, thinkrank: %d; init fired: %s; replayed: %s',
301 $d['api_available'] ? 'present' : 'missing',
302 '' !== $d['owner'] ? $d['owner'] : 'unknown',
303 $d['foreign'] ? ' (foreign copy — not ThinkRank\'s bundled runtime)' : '',
304 $d['total'],
305 $d['thinkrank'],
306 $d['hook_fired'] ? 'yes' : 'no',
307 $d['replayed'] ? 'yes' : 'no'
308 );
309 }
310
311 /**
312 * Register the ThinkRank ability category.
313 *
314 * @return void
315 */
316 public function register_category() {
317 if ( function_exists( 'wp_has_ability_category' ) && wp_has_ability_category( 'thinkrank' ) ) {
318 return;
319 }
320
321 if ( function_exists( 'wp_register_ability_category' ) ) {
322 wp_register_ability_category(
323 'thinkrank',
324 [
325 'label' => __( 'ThinkRank', 'thinkrank' ),
326 'description' => __( 'SEO settings, metadata, and analysis abilities powered by ThinkRank.', 'thinkrank' ),
327 ]
328 );
329 }
330 }
331
332 /**
333 * Register ThinkRank abilities.
334 *
335 * @return void
336 */
337 public function register_abilities() {
338 if ( ! Ability_Base::abilities_enabled() ) {
339 return;
340 }
341
342 $abilities = [
343 new List_Content_Types(),
344 new List_Content_Items(),
345 new Get_Global_Settings(),
346 new Update_Global_Settings(),
347 new Get_Post_Seo(),
348 new Update_Post_Seo(),
349 new List_Snippet_Issues(),
350 new Get_Duplicate_Snippets(),
351 new Get_Thin_Content(),
352 new Update_Thin_Content_Settings(),
353 new Get_Term_Seo(),
354 new Update_Term_Seo(),
355 new Get_Post_Seo_Checks(),
356 new Get_Term_Seo_Checks(),
357 new Get_Robots_Txt(),
358 new Update_Robots_Txt(),
359 new Get_Sitemap_Settings(),
360 new Update_Sitemap_Settings(),
361 new Get_Schema_Settings(),
362 new Update_Schema_Settings(),
363 new Get_Site_Identity_Settings(),
364 new Update_Site_Identity_Settings(),
365 new Get_Social_Meta_Settings(),
366 new Update_Social_Meta_Settings(),
367 new Get_Image_Seo_Settings(),
368 new Update_Image_Seo_Settings(),
369 new Get_Llms_Txt_Settings(),
370 new Update_Llms_Txt_Settings(),
371 new Get_Robots_Meta_Settings(),
372 new Get_External_Links_Settings(),
373 new Update_External_Links_Settings(),
374 new Get_Content_Type_Matrix(),
375 new Update_Content_Type_Matrix(),
376 new List_Images(),
377 new Get_Image_Alt_Text(),
378 new Update_Image_Alt_Text(),
379 new Fill_Missing_Alt_Text(),
380 new Get_Post_Content(),
381 new Get_FAQ(),
382 new Update_FAQ(),
383 new Purge_Caches(),
384 new Update_Robots_Meta_Settings(),
385 new Get_Ai_Budget(),
386 new Update_Ai_Budget(),
387 new Get_Instant_Indexing_Settings(),
388 new Update_Instant_Indexing_Settings(),
389 new Get_Author_Archives_Settings(),
390 new Update_Author_Archives_Settings(),
391 new Get_Email_Report_Settings(),
392 new Update_Email_Report_Settings(),
393 new Send_Email_Report_Test(),
394 new Get_Social_Platforms_Settings(),
395 new Update_Social_Platforms_Settings(),
396 new Submit_Urls_To_Index(),
397 new Get_Instant_Indexing_History(),
398 new Get_Llms_Txt_Status(),
399 new Generate_Llms_Txt(),
400 new Publish_Llms_Txt(),
401 new Get_Seo_Analytics_Data(),
402 new Get_Seo_Opportunities(),
403 new Get_Seo_Score(),
404 new Get_Seo_Analyzer(),
405 new Run_Seo_Analyzer(),
406 new Get_Performance_Data(),
407 new Get_Integrations_Status(),
408 new Get_Connection_Status(),
409 new Bulk_Analyze_And_Save(),
410 new Generate_Content_Brief(),
411 new Get_Pillar_Content(),
412 new Update_Pillar_Content(),
413 new Detect_Import_Sources(),
414 new Get_Import_Status(),
415 new Run_Seo_Import(),
416 new Preview_Seo_Import(),
417 ];
418
419 $abilities = apply_filters( 'thinkrank_register_abilities', $abilities );
420
421 foreach ( $abilities as $ability ) {
422 if ( ! $ability instanceof Ability_Base ) {
423 continue;
424 }
425
426 if ( ! $ability->meets_capability_policy() || ! $ability->is_enabled() ) {
427 continue;
428 }
429
430 if ( function_exists( 'wp_has_ability' ) && wp_has_ability( $ability->get_id() ) ) {
431 continue;
432 }
433
434 $ability->register();
435 }
436 }
437 }
438