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
← All changes | thinkrank.php +53 -6 2.6.0 → 2.11.0 View file →
@@ -3,9 +3,9 @@
3 3 /**
4 4 * Plugin Name: ThinkRank
5 5 * Plugin URI: https://thinkrank.ai/
6 6 * Description: AI-native SEO plugin for WordPress. Automate and enhance your SEO with cutting-edge AI while maintaining editorial control.
7 - * Version: 2.6.0
7 + * Version: 2.11.0
8 8 * Author: WPDeveloper
9 9 * Author URI: https://wpdeveloper.com/
10 10 * License: GPL v2 or later
11 11 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
@@ -14,9 +14,9 @@
14 14 * Requires at least: 6.0
15 15 * Requires PHP: 7.4
16 16 *
17 17 * @package ThinkRank
18 - * @version 2.6.0
18 + * @version 2.11.0
19 19 * @since 1.0.0
20 20 */
21 21
22 22 declare(strict_types=1);
@@ -26,9 +26,9 @@
26 26 exit;
27 27 }
28 28
29 29 // Define plugin constants
30 -define('THINKRANK_VERSION', '2.6.0');
30 +define('THINKRANK_VERSION', '2.11.0');
31 31 define('THINKRANK_PLUGIN_FILE', __FILE__);
32 32 define('THINKRANK_PLUGIN_DIR', plugin_dir_path(__FILE__));
33 33 define('THINKRANK_PLUGIN_URL', plugin_dir_url(__FILE__));
34 34 define('THINKRANK_PLUGIN_BASENAME', plugin_basename(__FILE__));
@@ -188,9 +188,10 @@
188 188 }
189 189 }
190 190
191 191 /**
192 - * Register the sitemap regeneration WP-Cron listeners.
192 + * Register the sitemap regeneration WP-Cron listeners, and the
193 + * content-change listeners that queue them.
193 194 *
194 195 * Runs on plugins_loaded (via init()), so the callbacks exist on every
195 196 * request — including WP-Cron, which never fires rest_api_init and therefore
196 197 * never builds the Sitemap REST endpoint (whose constructor would otherwise
@@ -197,11 +198,17 @@
197 198 * be the only place the scheduled regeneration hooks get a listener). The
198 199 * generator is built lazily inside the callback so this stays cheap on the
199 200 * vast majority of requests where no regeneration is due.
200 201 *
202 + * The content-change listeners need the same treatment for the same
203 + * reason: WP-Cron publishes scheduled posts, and Quick Edit, the classic
204 + * editor and WP-CLI change content without REST either (#824).
205 + *
201 206 * @return void
202 207 */
203 208 private function register_sitemap_cron_listeners(): void {
209 + ThinkRank\SEO\Sitemap_Generator::register_content_listeners();
210 +
204 211 add_action('thinkrank_regenerate_sitemap', static function () {
205 212 (new ThinkRank\SEO\Sitemap_Generator())->auto_regenerate_sitemap();
206 213 });
207 214 add_action('thinkrank_regenerate_sitemap_settings', static function () {
@@ -233,15 +240,33 @@
233 240 // is_admin() is true for admin-ajax.php and REST_REQUEST for public
234 241 // core routes, both of which anonymous front-end traffic reaches — so
235 242 // without the logged-in test a visitor could still pay for the rebuild
236 243 // this method documents as never being theirs to pay for. WP-CLI has no
237 - // user, and is trusted by definition.
244 + // user, and is trusted by definition. A cron run is the process this
245 + // work belongs to in the first place: a scheduled event that was
246 + // consumed while another request held the generation lock is never
247 + // re-queued, so without this a site whose admins see only failures
248 + // could not recover through a (often memory-unlimited) system cron.
249 + //
250 + // An MCP call counts too. It is an admin-equivalent action, but it is
251 + // dispatched from `parse_request` on the pretty /thinkrank/mcp route,
252 + // so it is neither is_admin() nor REST_REQUEST and never qualified —
253 + // which meant an agent could change sitemap settings on a site with no
254 + // working cron and leave the served file stale indefinitely, having
255 + // already reported success (#764). The logged-in test still applies:
256 + // Mcp_Server sets the current user only after a credential validates,
257 + // so an anonymous POST to that route is not eligible.
258 + $is_mcp_request = class_exists('ThinkRank\Mcp\Mcp_Manager')
259 + && ThinkRank\Mcp\Mcp_Manager::is_serving_request();
260 +
238 261 $eligible = (defined('WP_CLI') && WP_CLI)
262 + || wp_doing_cron()
239 263 || (
240 264 is_user_logged_in()
241 265 && (
242 266 is_admin()
243 267 || (defined('REST_REQUEST') && REST_REQUEST)
268 + || $is_mcp_request
244 269 )
245 270 );
246 271
247 272 if (!$eligible) {
@@ -333,9 +358,17 @@
333 358 */
334 359 private function load_components(): void {
335 360 $this->components = [
336 361 'database' => new ThinkRank\Core\Database(),
337 - 'settings' => new ThinkRank\Core\Settings(),
362 + // Settings::instance() rather than a fresh object: the container
363 + // and the singleton were otherwise two different Settings, each
364 + // with its own memo, because instance() resolves the container and
365 + // this array is assigned only after every constructor above has
366 + // run. Anything resolving the singleton during that window got a
367 + // standalone instance that outlived the request, so a hook
368 + // registered from the component's init() ran against an object no
369 + // consumer read through (#516).
370 + 'settings' => ThinkRank\Core\Settings::instance(),
338 371 'role_manager' => new ThinkRank\Core\Role_Manager(),
339 372 'security_headers' => new ThinkRank\Core\Security_Headers(),
340 373 'asset_optimizer' => new ThinkRank\Core\Asset_Optimizer(),
341 374 'usage_tracker' => new ThinkRank\Core\Usage_Tracker_Manager(),
@@ -348,18 +381,32 @@
348 381 'ai' => new ThinkRank\AI\Manager(),
349 382 'frontend_seo' => new ThinkRank\Frontend\SEO_Manager(),
350 383 'seo_notice' => new ThinkRank\Admin\SEO_Notice(),
351 384 'search_visibility_notice' => new ThinkRank\Admin\Search_Visibility_Notice(),
385 + 'webroot_writable_notice' => new ThinkRank\Admin\Webroot_Writable_Notice(),
352 386 'performance_collector' => new ThinkRank\SEO\Performance_Data_Collector(),
387 + 'query_guard' => new ThinkRank\SEO\Query_Guard(),
388 + 'feeds' => new ThinkRank\SEO\Feed_Manager(),
389 + 'sitemap_stylesheet' => new ThinkRank\SEO\Sitemap_Stylesheet(),
390 + 'oembed' => new ThinkRank\SEO\Oembed_Manager(),
391 + 'content_visibility' => new ThinkRank\SEO\Content_Visibility(),
353 392 'instant_indexing' => new ThinkRank\SEO\Instant_Indexing_Manager(),
354 393 'instant_indexing_reconciler' => new ThinkRank\SEO\Instant_Indexing_Reconciler(),
355 394 'author_archives' => new ThinkRank\SEO\Author_Archives_Manager(),
356 395 'seo_analyzer' => new ThinkRank\SEO\SEO_Analyzer(),
396 + // Bulk Snippets' persisted issue index: invalidation hooks must run
397 + // on every request, since posts are edited everywhere but there.
398 + 'snippet_index' => new ThinkRank\SEO\Snippet_Index(),
399 + // The thin content report's word counts: same reason, and a
400 + // separate index because a word count and a snippet verdict go
401 + // stale for different reasons (#565).
402 + 'word_count_index' => new ThinkRank\SEO\Word_Count_Index(),
357 403 'email_report' => new ThinkRank\SEO\Email_Report_Manager(),
358 404 'google_oauth' => new ThinkRank\Integrations\Google_OAuth_Proxy(),
359 405 'multilingual' => new ThinkRank\Integrations\Multilingual_Manager(),
360 406 'ai_traffic' => new ThinkRank\SEO\Ai_Traffic_Tracker(),
361 407 'analytics' => new ThinkRank\SEO\Analytics_Manager(),
408 + 'schema_conflict_health_check' => new ThinkRank\Diagnostics\Schema_Conflict_Health_Check(),
362 409 'abilities' => new ThinkRank\Abilities\Abilities_Registrar(),
363 410 'mcp' => new ThinkRank\Mcp\Mcp_Manager(),
364 411 ];
365 412 }