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 | thinkrank.php +152 -7 1.26.0 → 2.10.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: 1.26.0
7 + * Version: 2.10.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
@@ -11,12 +11,12 @@
11 11 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
12 12 * Text Domain: thinkrank
13 13 * Domain Path: /languages
14 14 * Requires at least: 6.0
15 - * Requires PHP: 8.0
15 + * Requires PHP: 7.4
16 16 *
17 17 * @package ThinkRank
18 - * @version 1.26.0
18 + * @version 2.10.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', '1.26.0');
30 +define('THINKRANK_VERSION', '2.10.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__));
@@ -33,12 +33,12 @@
33 33 define('THINKRANK_PLUGIN_URL', plugin_dir_url(__FILE__));
34 34 define('THINKRANK_PLUGIN_BASENAME', plugin_basename(__FILE__));
35 35
36 36 // Minimum requirements check
37 -if (version_compare(PHP_VERSION, '8.0', '<')) {
37 +if (version_compare(PHP_VERSION, '7.4', '<')) {
38 38 add_action('admin_notices', function () {
39 39 echo '<div class="notice notice-error"><p>';
40 - echo esc_html__('ThinkRank requires PHP 8.0 or higher. Please upgrade your PHP version.', 'thinkrank');
40 + echo esc_html__('ThinkRank requires PHP 7.4 or higher. Please upgrade your PHP version.', 'thinkrank');
41 41 echo '</p></div>';
42 42 });
43 43 return;
44 44 }
@@ -120,8 +120,10 @@
120 120 }
121 121
122 122 /**
123 123 * Prevent unserialization
124 + *
125 + * @throws \Exception On failure.
124 126 */
125 127 public function __wakeup() {
126 128 throw new \Exception('Cannot unserialize singleton');
127 129 }
@@ -174,8 +176,9 @@
174 176 public function init(): void {
175 177 try {
176 178 $this->maybe_update_database();
177 179 $this->register_sitemap_cron_listeners();
180 + $this->clear_retired_brand_visibility_cron();
178 181 $this->load_components();
179 182 $this->init_components();
180 183 $this->load_template_functions();
181 184
@@ -203,11 +206,124 @@
203 206 });
204 207 add_action('thinkrank_regenerate_sitemap_settings', static function () {
205 208 (new ThinkRank\SEO\Sitemap_Generator())->regenerate_sitemap_from_settings();
206 209 });
210 +
211 + add_action('shutdown', [$this, 'maybe_take_over_sitemap_regeneration'], 100);
207 212 }
208 213
209 214 /**
215 + * Rebuild the sitemap in-request when WP-Cron has not delivered.
216 + *
217 + * WP-Cron only runs when a request arrives, so with DISABLE_WP_CRON set, a
218 + * host blocking loopback requests, or very little traffic, the scheduled
219 + * regeneration never fires and the sitemap silently stops updating (#629).
220 + * Once the event is overdue by the generator's grace period, an admin, REST
221 + * or WP-CLI request takes the work over so the site converges on its own.
222 + *
223 + * Front-end requests are deliberately excluded: this runs on `shutdown`,
224 + * after the response, but generation on a large site is not free and a
225 + * visitor should never pay for it. Admin traffic is what a site with broken
226 + * cron reliably still has — the sitemap goes stale right after someone
227 + * publishes something, and that someone is in wp-admin.
228 + *
229 + * @since 2.2.1
230 + * @return void
231 + */
232 + public function maybe_take_over_sitemap_regeneration(): void {
233 + // is_admin() is true for admin-ajax.php and REST_REQUEST for public
234 + // core routes, both of which anonymous front-end traffic reaches — so
235 + // without the logged-in test a visitor could still pay for the rebuild
236 + // this method documents as never being theirs to pay for. WP-CLI has no
237 + // user, and is trusted by definition.
238 + //
239 + // An MCP call counts too. It is an admin-equivalent action, but it is
240 + // dispatched from `parse_request` on the pretty /thinkrank/mcp route,
241 + // so it is neither is_admin() nor REST_REQUEST and never qualified —
242 + // which meant an agent could change sitemap settings on a site with no
243 + // working cron and leave the served file stale indefinitely, having
244 + // already reported success (#764). The logged-in test still applies:
245 + // Mcp_Server sets the current user only after a credential validates,
246 + // so an anonymous POST to that route is not eligible.
247 + $is_mcp_request = class_exists('ThinkRank\Mcp\Mcp_Manager')
248 + && ThinkRank\Mcp\Mcp_Manager::is_serving_request();
249 +
250 + $eligible = (defined('WP_CLI') && WP_CLI)
251 + || (
252 + is_user_logged_in()
253 + && (
254 + is_admin()
255 + || (defined('REST_REQUEST') && REST_REQUEST)
256 + || $is_mcp_request
257 + )
258 + );
259 +
260 + if (!$eligible) {
261 + return;
262 + }
263 +
264 + // Cheap autoloaded-option read, so the vast majority of requests stop
265 + // here without building the generator.
266 + if (!ThinkRank\SEO\Sitemap_Generator::has_overdue_regeneration()) {
267 + return;
268 + }
269 +
270 + // `shutdown` runs after the output buffers are flushed, but flushed is
271 + // not delivered: on FPM the connection stays open until the process
272 + // ends, so without this the browser — or the REST call the sitemap
273 + // screen just made — waits out the whole generation. Hand the response
274 + // back first, then rebuild.
275 + $this->close_request();
276 +
277 + // Read-only construction: the auto-generation hooks are pointless this
278 + // late in the request and would only add duplicate callbacks.
279 + (new ThinkRank\SEO\Sitemap_Generator(false))->run_overdue_regeneration();
280 + }
281 +
282 + /**
283 + * Deliver the response and let the request keep working without the client.
284 + *
285 + * A no-op on SAPIs that cannot do it, where the caller simply pays for the
286 + * work as before.
287 + *
288 + * @since 2.2.1
289 + * @return void
290 + */
291 + private function close_request(): void {
292 + if (defined('WP_CLI') && WP_CLI) {
293 + return;
294 + }
295 +
296 + if (function_exists('fastcgi_finish_request')) {
297 + fastcgi_finish_request();
298 + return;
299 + }
300 +
301 + if (function_exists('litespeed_finish_request')) {
302 + litespeed_finish_request();
303 + }
304 + }
305 +
306 + /**
307 + * Unschedule the cron events left behind by the removed Brand Visibility
308 + * feature.
309 + *
310 + * A site that ran a Brand Visibility check can still carry its drain tick
311 + * and watchdog. Their callbacks, and the watchdog's custom recurrence, no
312 + * longer exist, so the events would only fire into nothing.
313 + * wp_unschedule_hook() walks the autoloaded cron array and writes nothing
314 + * when the hook is absent, so this costs nothing once they are gone.
315 + * Deactivation and uninstall clear the same hooks (cleanup-manifest.php).
316 + *
317 + * @return void
318 + */
319 + private function clear_retired_brand_visibility_cron(): void {
320 + foreach (['thinkrank_bv_tick', 'thinkrank_bv_watchdog'] as $hook) {
321 + wp_unschedule_hook($hook);
322 + }
323 + }
324 +
325 + /**
210 326 * Check if database schema needs updating and run migrations
211 327 *
212 328 * Standard WordPress pattern: compare stored db_version against current,
213 329 * run dbDelta if stale. This handles schema changes (new tables, new columns)
@@ -230,9 +346,17 @@
230 346 */
231 347 private function load_components(): void {
232 348 $this->components = [
233 349 'database' => new ThinkRank\Core\Database(),
234 - 'settings' => new ThinkRank\Core\Settings(),
350 + // Settings::instance() rather than a fresh object: the container
351 + // and the singleton were otherwise two different Settings, each
352 + // with its own memo, because instance() resolves the container and
353 + // this array is assigned only after every constructor above has
354 + // run. Anything resolving the singleton during that window got a
355 + // standalone instance that outlived the request, so a hook
356 + // registered from the component's init() ran against an object no
357 + // consumer read through (#516).
358 + 'settings' => ThinkRank\Core\Settings::instance(),
235 359 'role_manager' => new ThinkRank\Core\Role_Manager(),
236 360 'security_headers' => new ThinkRank\Core\Security_Headers(),
237 361 'asset_optimizer' => new ThinkRank\Core\Asset_Optimizer(),
238 362 'usage_tracker' => new ThinkRank\Core\Usage_Tracker_Manager(),
@@ -239,18 +363,38 @@
239 363 'api' => new ThinkRank\API\Manager(),
240 364 'admin' => new ThinkRank\Admin\Manager(),
241 365 'blocks' => new ThinkRank\Editor\Blocks_Manager(),
242 366 'elementor' => new ThinkRank\Editor\Elementor_Manager(),
367 + 'bricks_elements' => new ThinkRank\Editor\Bricks_Elements_Manager(),
368 + 'beaver_modules' => new ThinkRank\Editor\Beaver_Modules_Manager(),
243 369 'ai' => new ThinkRank\AI\Manager(),
244 370 'frontend_seo' => new ThinkRank\Frontend\SEO_Manager(),
245 371 'seo_notice' => new ThinkRank\Admin\SEO_Notice(),
372 + 'search_visibility_notice' => new ThinkRank\Admin\Search_Visibility_Notice(),
373 + 'webroot_writable_notice' => new ThinkRank\Admin\Webroot_Writable_Notice(),
246 374 'performance_collector' => new ThinkRank\SEO\Performance_Data_Collector(),
375 + 'query_guard' => new ThinkRank\SEO\Query_Guard(),
376 + 'feeds' => new ThinkRank\SEO\Feed_Manager(),
377 + 'sitemap_stylesheet' => new ThinkRank\SEO\Sitemap_Stylesheet(),
378 + 'oembed' => new ThinkRank\SEO\Oembed_Manager(),
379 + 'content_visibility' => new ThinkRank\SEO\Content_Visibility(),
247 380 'instant_indexing' => new ThinkRank\SEO\Instant_Indexing_Manager(),
381 + 'instant_indexing_reconciler' => new ThinkRank\SEO\Instant_Indexing_Reconciler(),
248 382 'author_archives' => new ThinkRank\SEO\Author_Archives_Manager(),
383 + 'seo_analyzer' => new ThinkRank\SEO\SEO_Analyzer(),
384 + // Bulk Snippets' persisted issue index: invalidation hooks must run
385 + // on every request, since posts are edited everywhere but there.
386 + 'snippet_index' => new ThinkRank\SEO\Snippet_Index(),
387 + // The thin content report's word counts: same reason, and a
388 + // separate index because a word count and a snippet verdict go
389 + // stale for different reasons (#565).
390 + 'word_count_index' => new ThinkRank\SEO\Word_Count_Index(),
249 391 'email_report' => new ThinkRank\SEO\Email_Report_Manager(),
250 392 'google_oauth' => new ThinkRank\Integrations\Google_OAuth_Proxy(),
251 393 'multilingual' => new ThinkRank\Integrations\Multilingual_Manager(),
394 + 'ai_traffic' => new ThinkRank\SEO\Ai_Traffic_Tracker(),
252 395 'analytics' => new ThinkRank\SEO\Analytics_Manager(),
396 + 'schema_conflict_health_check' => new ThinkRank\Diagnostics\Schema_Conflict_Health_Check(),
253 397 'abilities' => new ThinkRank\Abilities\Abilities_Registrar(),
254 398 'mcp' => new ThinkRank\Mcp\Mcp_Manager(),
255 399 ];
256 400 }
@@ -419,8 +563,9 @@
419 563 * Initialize the plugin
420 564 *
421 565 * @return ThinkRank
422 566 */
567 +// phpcs:ignore Universal.Files.SeparateFunctionsFromOO.Mixed -- plugin bootstrap: the accessor belongs next to the class it returns.
423 568 function thinkrank(): ThinkRank {
424 569 return ThinkRank::get_instance();
425 570 }
426 571