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 +153 -26 1.29.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: 1.29.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
@@ -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.29.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', '1.29.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__));
@@ -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 }
@@ -176,9 +176,9 @@
176 176 public function init(): void {
177 177 try {
178 178 $this->maybe_update_database();
179 179 $this->register_sitemap_cron_listeners();
180 - $this->register_brand_visibility_cron();
180 + $this->clear_retired_brand_visibility_cron();
181 181 $this->load_components();
182 182 $this->init_components();
183 183 $this->load_template_functions();
184 184
@@ -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 () {
@@ -206,35 +213,129 @@
206 213 });
207 214 add_action('thinkrank_regenerate_sitemap_settings', static function () {
208 215 (new ThinkRank\SEO\Sitemap_Generator())->regenerate_sitemap_from_settings();
209 216 });
217 +
218 + add_action('shutdown', [$this, 'maybe_take_over_sitemap_regeneration'], 100);
210 219 }
211 220
212 221 /**
213 - * Register the Brand Visibility run-drain listener.
222 + * Rebuild the sitemap in-request when WP-Cron has not delivered.
214 223 *
215 - * Runs on plugins_loaded (via init()) rather than from the REST endpoint,
216 - * because the ticks that drain a run are WP-Cron requests — they never
217 - * reach rest_api_init, so registering the listener there would mean a run
218 - * starts and then never progresses.
224 + * WP-Cron only runs when a request arrives, so with DISABLE_WP_CRON set, a
225 + * host blocking loopback requests, or very little traffic, the scheduled
226 + * regeneration never fires and the sitemap silently stops updating (#629).
227 + * Once the event is overdue by the generator's grace period, an admin, REST
228 + * or WP-CLI request takes the work over so the site converges on its own.
219 229 *
230 + * Front-end requests are deliberately excluded: this runs on `shutdown`,
231 + * after the response, but generation on a large site is not free and a
232 + * visitor should never pay for it. Admin traffic is what a site with broken
233 + * cron reliably still has — the sitemap goes stale right after someone
234 + * publishes something, and that someone is in wp-admin.
235 + *
236 + * @since 2.2.1
220 237 * @return void
221 238 */
222 - private function register_brand_visibility_cron(): void {
223 - add_action(ThinkRank\AI\Brand_Visibility_Runner::TICK_HOOK, static function () {
224 - (new ThinkRank\AI\Brand_Visibility_Runner())->tick();
225 - });
239 + public function maybe_take_over_sitemap_regeneration(): void {
240 + // is_admin() is true for admin-ajax.php and REST_REQUEST for public
241 + // core routes, both of which anonymous front-end traffic reaches — so
242 + // without the logged-in test a visitor could still pay for the rebuild
243 + // this method documents as never being theirs to pay for. WP-CLI has no
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();
226 260
227 - // Safety net: a tick killed by a fatal or a worker timeout never
228 - // reaches its own reschedule, which would strand the run. The
229 - // watchdog re-arms the drain and unschedules itself when idle.
230 - add_filter('cron_schedules', [ThinkRank\AI\Brand_Visibility_Runner::class, 'add_cron_interval']); // phpcs:ignore WordPress.WP.CronInterval.ChangeDetected
231 - add_action(ThinkRank\AI\Brand_Visibility_Runner::WATCHDOG_HOOK, static function () {
232 - (new ThinkRank\AI\Brand_Visibility_Runner())->watchdog();
233 - });
261 + $eligible = (defined('WP_CLI') && WP_CLI)
262 + || wp_doing_cron()
263 + || (
264 + is_user_logged_in()
265 + && (
266 + is_admin()
267 + || (defined('REST_REQUEST') && REST_REQUEST)
268 + || $is_mcp_request
269 + )
270 + );
271 +
272 + if (!$eligible) {
273 + return;
274 + }
275 +
276 + // Cheap autoloaded-option read, so the vast majority of requests stop
277 + // here without building the generator.
278 + if (!ThinkRank\SEO\Sitemap_Generator::has_overdue_regeneration()) {
279 + return;
280 + }
281 +
282 + // `shutdown` runs after the output buffers are flushed, but flushed is
283 + // not delivered: on FPM the connection stays open until the process
284 + // ends, so without this the browser — or the REST call the sitemap
285 + // screen just made — waits out the whole generation. Hand the response
286 + // back first, then rebuild.
287 + $this->close_request();
288 +
289 + // Read-only construction: the auto-generation hooks are pointless this
290 + // late in the request and would only add duplicate callbacks.
291 + (new ThinkRank\SEO\Sitemap_Generator(false))->run_overdue_regeneration();
234 292 }
235 293
236 294 /**
295 + * Deliver the response and let the request keep working without the client.
296 + *
297 + * A no-op on SAPIs that cannot do it, where the caller simply pays for the
298 + * work as before.
299 + *
300 + * @since 2.2.1
301 + * @return void
302 + */
303 + private function close_request(): void {
304 + if (defined('WP_CLI') && WP_CLI) {
305 + return;
306 + }
307 +
308 + if (function_exists('fastcgi_finish_request')) {
309 + fastcgi_finish_request();
310 + return;
311 + }
312 +
313 + if (function_exists('litespeed_finish_request')) {
314 + litespeed_finish_request();
315 + }
316 + }
317 +
318 + /**
319 + * Unschedule the cron events left behind by the removed Brand Visibility
320 + * feature.
321 + *
322 + * A site that ran a Brand Visibility check can still carry its drain tick
323 + * and watchdog. Their callbacks, and the watchdog's custom recurrence, no
324 + * longer exist, so the events would only fire into nothing.
325 + * wp_unschedule_hook() walks the autoloaded cron array and writes nothing
326 + * when the hook is absent, so this costs nothing once they are gone.
327 + * Deactivation and uninstall clear the same hooks (cleanup-manifest.php).
328 + *
329 + * @return void
330 + */
331 + private function clear_retired_brand_visibility_cron(): void {
332 + foreach (['thinkrank_bv_tick', 'thinkrank_bv_watchdog'] as $hook) {
333 + wp_unschedule_hook($hook);
334 + }
335 + }
336 +
337 + /**
237 338 * Check if database schema needs updating and run migrations
238 339 *
239 340 * Standard WordPress pattern: compare stored db_version against current,
240 341 * run dbDelta if stale. This handles schema changes (new tables, new columns)
@@ -257,9 +358,17 @@
257 358 */
258 359 private function load_components(): void {
259 360 $this->components = [
260 361 'database' => new ThinkRank\Core\Database(),
261 - '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(),
262 371 'role_manager' => new ThinkRank\Core\Role_Manager(),
263 372 'security_headers' => new ThinkRank\Core\Security_Headers(),
264 373 'asset_optimizer' => new ThinkRank\Core\Asset_Optimizer(),
265 374 'usage_tracker' => new ThinkRank\Core\Usage_Tracker_Manager(),
@@ -266,20 +375,38 @@
266 375 'api' => new ThinkRank\API\Manager(),
267 376 'admin' => new ThinkRank\Admin\Manager(),
268 377 'blocks' => new ThinkRank\Editor\Blocks_Manager(),
269 378 'elementor' => new ThinkRank\Editor\Elementor_Manager(),
379 + 'bricks_elements' => new ThinkRank\Editor\Bricks_Elements_Manager(),
380 + 'beaver_modules' => new ThinkRank\Editor\Beaver_Modules_Manager(),
270 381 'ai' => new ThinkRank\AI\Manager(),
271 382 'frontend_seo' => new ThinkRank\Frontend\SEO_Manager(),
272 383 'seo_notice' => new ThinkRank\Admin\SEO_Notice(),
384 + 'search_visibility_notice' => new ThinkRank\Admin\Search_Visibility_Notice(),
385 + 'webroot_writable_notice' => new ThinkRank\Admin\Webroot_Writable_Notice(),
273 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(),
274 392 'instant_indexing' => new ThinkRank\SEO\Instant_Indexing_Manager(),
393 + 'instant_indexing_reconciler' => new ThinkRank\SEO\Instant_Indexing_Reconciler(),
275 394 'author_archives' => new ThinkRank\SEO\Author_Archives_Manager(),
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(),
276 403 'email_report' => new ThinkRank\SEO\Email_Report_Manager(),
277 404 'google_oauth' => new ThinkRank\Integrations\Google_OAuth_Proxy(),
278 405 'multilingual' => new ThinkRank\Integrations\Multilingual_Manager(),
279 406 'ai_traffic' => new ThinkRank\SEO\Ai_Traffic_Tracker(),
280 - 'auto_ai' => new ThinkRank\SEO\Auto_Ai_Optimizer(),
281 407 'analytics' => new ThinkRank\SEO\Analytics_Manager(),
408 + 'schema_conflict_health_check' => new ThinkRank\Diagnostics\Schema_Conflict_Health_Check(),
282 409 'abilities' => new ThinkRank\Abilities\Abilities_Registrar(),
283 410 'mcp' => new ThinkRank\Mcp\Mcp_Manager(),
284 411 ];
285 412 }