PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.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 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
← All changes | thinkrank.php +261 -14 1.10.02.7.0 View file →
@@ -1,23 +1,22 @@
1 1 <?php
2 2
3 3 /**
4 4 * Plugin Name: ThinkRank
5 - * Plugin URI: https://thinkrank.ai/wordpress-plugin
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.10.0
8 - * Author: rupok
9 - * Author URI: https://thinkrank.ai
7 + * Version: 2.7.0
8 + * Author: WPDeveloper
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
12 12 * Text Domain: thinkrank
13 13 * Domain Path: /languages
14 14 * Requires at least: 6.0
15 - * Tested up to: 6.9
16 - * Requires PHP: 8.0
15 + * Requires PHP: 7.4
17 16 *
18 17 * @package ThinkRank
19 - * @version 1.10.0
18 + * @version 2.7.0
20 19 * @since 1.0.0
21 20 */
22 21
23 22 declare(strict_types=1);
@@ -27,9 +26,9 @@
27 26 exit;
28 27 }
29 28
30 29 // Define plugin constants
31 -define('THINKRANK_VERSION', '1.10.0');
30 +define('THINKRANK_VERSION', '2.7.0');
32 31 define('THINKRANK_PLUGIN_FILE', __FILE__);
33 32 define('THINKRANK_PLUGIN_DIR', plugin_dir_path(__FILE__));
34 33 define('THINKRANK_PLUGIN_URL', plugin_dir_url(__FILE__));
35 34 define('THINKRANK_PLUGIN_BASENAME', plugin_basename(__FILE__));
@@ -34,12 +33,12 @@
34 33 define('THINKRANK_PLUGIN_URL', plugin_dir_url(__FILE__));
35 34 define('THINKRANK_PLUGIN_BASENAME', plugin_basename(__FILE__));
36 35
37 36 // Minimum requirements check
38 -if (version_compare(PHP_VERSION, '8.0', '<')) {
37 +if (version_compare(PHP_VERSION, '7.4', '<')) {
39 38 add_action('admin_notices', function () {
40 39 echo '<div class="notice notice-error"><p>';
41 - 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');
42 41 echo '</p></div>';
43 42 });
44 43 return;
45 44 }
@@ -56,8 +55,23 @@
56 55 // Autoloader
57 56 require_once THINKRANK_PLUGIN_DIR . 'includes/class-autoloader.php';
58 57
59 58 /**
59 + * Bundled AI Building Blocks (Abilities API + MCP Adapter).
60 + *
61 + * Loaded through the Jetpack Autoloader so that if the same libraries are also
62 + * shipped by another plugin — or land in WordPress core — the newest copy wins
63 + * and loads once, with no fatal class collisions. This lets ThinkRank serve its
64 + * MCP endpoint out of the box, without requiring the standalone MCP Adapter and
65 + * Abilities API plugins. See docs/mcp-server.md for the update procedure.
66 + */
67 +$thinkrank_mcp_runtime = THINKRANK_PLUGIN_DIR . 'dependencies/vendor/autoload_packages.php';
68 +if (is_readable($thinkrank_mcp_runtime)) {
69 + require_once $thinkrank_mcp_runtime;
70 +}
71 +unset($thinkrank_mcp_runtime);
72 +
73 +/**
60 74 * Main ThinkRank Plugin Class
61 75 *
62 76 * Follows Single Responsibility Principle - only handles plugin initialization
63 77 *
@@ -106,8 +120,10 @@
106 120 }
107 121
108 122 /**
109 123 * Prevent unserialization
124 + *
125 + * @throws \Exception On failure.
110 126 */
111 127 public function __wakeup() {
112 128 throw new \Exception('Cannot unserialize singleton');
113 129 }
@@ -130,11 +146,30 @@
130 146 register_activation_hook(__FILE__, [$this, 'activate']);
131 147 register_deactivation_hook(__FILE__, [$this, 'deactivate']);
132 148
133 149 add_action('plugins_loaded', [$this, 'init']);
150 + add_filter('plugin_action_links_' . THINKRANK_PLUGIN_BASENAME, [$this, 'add_action_links']);
134 151 }
135 152
136 153 /**
154 + * Add a "Dashboard" link to the plugin action links on the Plugins page.
155 + *
156 + * @param array $links Existing plugin action links.
157 + * @return array
158 + */
159 + public function add_action_links(array $links): array {
160 + $dashboard_link = sprintf(
161 + '<a href="%s">%s</a>',
162 + esc_url(admin_url('admin.php?page=thinkrank')),
163 + esc_html__('Dashboard', 'thinkrank')
164 + );
165 +
166 + array_unshift($links, $dashboard_link);
167 +
168 + return $links;
169 + }
170 +
171 + /**
137 172 * Initialize plugin components
138 173 *
139 174 * @return void
140 175 */
@@ -139,8 +174,11 @@
139 174 * @return void
140 175 */
141 176 public function init(): void {
142 177 try {
178 + $this->maybe_update_database();
179 + $this->register_sitemap_cron_listeners();
180 + $this->clear_retired_brand_visibility_cron();
143 181 $this->load_components();
144 182 $this->init_components();
145 183 $this->load_template_functions();
146 184
@@ -150,8 +188,146 @@
150 188 }
151 189 }
152 190
153 191 /**
192 + * Register the sitemap regeneration WP-Cron listeners.
193 + *
194 + * Runs on plugins_loaded (via init()), so the callbacks exist on every
195 + * request — including WP-Cron, which never fires rest_api_init and therefore
196 + * never builds the Sitemap REST endpoint (whose constructor would otherwise
197 + * be the only place the scheduled regeneration hooks get a listener). The
198 + * generator is built lazily inside the callback so this stays cheap on the
199 + * vast majority of requests where no regeneration is due.
200 + *
201 + * @return void
202 + */
203 + private function register_sitemap_cron_listeners(): void {
204 + add_action('thinkrank_regenerate_sitemap', static function () {
205 + (new ThinkRank\SEO\Sitemap_Generator())->auto_regenerate_sitemap();
206 + });
207 + add_action('thinkrank_regenerate_sitemap_settings', static function () {
208 + (new ThinkRank\SEO\Sitemap_Generator())->regenerate_sitemap_from_settings();
209 + });
210 +
211 + add_action('shutdown', [$this, 'maybe_take_over_sitemap_regeneration'], 100);
212 + }
213 +
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 + $eligible = (defined('WP_CLI') && WP_CLI)
239 + || (
240 + is_user_logged_in()
241 + && (
242 + is_admin()
243 + || (defined('REST_REQUEST') && REST_REQUEST)
244 + )
245 + );
246 +
247 + if (!$eligible) {
248 + return;
249 + }
250 +
251 + // Cheap autoloaded-option read, so the vast majority of requests stop
252 + // here without building the generator.
253 + if (!ThinkRank\SEO\Sitemap_Generator::has_overdue_regeneration()) {
254 + return;
255 + }
256 +
257 + // `shutdown` runs after the output buffers are flushed, but flushed is
258 + // not delivered: on FPM the connection stays open until the process
259 + // ends, so without this the browser — or the REST call the sitemap
260 + // screen just made — waits out the whole generation. Hand the response
261 + // back first, then rebuild.
262 + $this->close_request();
263 +
264 + // Read-only construction: the auto-generation hooks are pointless this
265 + // late in the request and would only add duplicate callbacks.
266 + (new ThinkRank\SEO\Sitemap_Generator(false))->run_overdue_regeneration();
267 + }
268 +
269 + /**
270 + * Deliver the response and let the request keep working without the client.
271 + *
272 + * A no-op on SAPIs that cannot do it, where the caller simply pays for the
273 + * work as before.
274 + *
275 + * @since 2.2.1
276 + * @return void
277 + */
278 + private function close_request(): void {
279 + if (defined('WP_CLI') && WP_CLI) {
280 + return;
281 + }
282 +
283 + if (function_exists('fastcgi_finish_request')) {
284 + fastcgi_finish_request();
285 + return;
286 + }
287 +
288 + if (function_exists('litespeed_finish_request')) {
289 + litespeed_finish_request();
290 + }
291 + }
292 +
293 + /**
294 + * Unschedule the cron events left behind by the removed Brand Visibility
295 + * feature.
296 + *
297 + * A site that ran a Brand Visibility check can still carry its drain tick
298 + * and watchdog. Their callbacks, and the watchdog's custom recurrence, no
299 + * longer exist, so the events would only fire into nothing.
300 + * wp_unschedule_hook() walks the autoloaded cron array and writes nothing
301 + * when the hook is absent, so this costs nothing once they are gone.
302 + * Deactivation and uninstall clear the same hooks (cleanup-manifest.php).
303 + *
304 + * @return void
305 + */
306 + private function clear_retired_brand_visibility_cron(): void {
307 + foreach (['thinkrank_bv_tick', 'thinkrank_bv_watchdog'] as $hook) {
308 + wp_unschedule_hook($hook);
309 + }
310 + }
311 +
312 + /**
313 + * Check if database schema needs updating and run migrations
314 + *
315 + * Standard WordPress pattern: compare stored db_version against current,
316 + * run dbDelta if stale. This handles schema changes (new tables, new columns)
317 + * without requiring plugin deactivation/reactivation.
318 + *
319 + * @since 1.10.0
320 + * @return void
321 + */
322 + private function maybe_update_database(): void {
323 + $schema = new ThinkRank\Database\Database_Schema();
324 + if ($schema->needs_update()) {
325 + $schema->create_tables();
326 + }
327 + }
328 +
329 + /**
154 330 * Load plugin components (Dependency Injection Container pattern)
155 331 *
156 332 * @return void
157 333 */
@@ -158,19 +334,39 @@
158 334 private function load_components(): void {
159 335 $this->components = [
160 336 'database' => new ThinkRank\Core\Database(),
161 337 'settings' => new ThinkRank\Core\Settings(),
338 + 'role_manager' => new ThinkRank\Core\Role_Manager(),
162 339 'security_headers' => new ThinkRank\Core\Security_Headers(),
163 340 'asset_optimizer' => new ThinkRank\Core\Asset_Optimizer(),
341 + 'usage_tracker' => new ThinkRank\Core\Usage_Tracker_Manager(),
164 342 'api' => new ThinkRank\API\Manager(),
165 343 'admin' => new ThinkRank\Admin\Manager(),
344 + 'blocks' => new ThinkRank\Editor\Blocks_Manager(),
345 + 'elementor' => new ThinkRank\Editor\Elementor_Manager(),
346 + 'bricks_elements' => new ThinkRank\Editor\Bricks_Elements_Manager(),
347 + 'beaver_modules' => new ThinkRank\Editor\Beaver_Modules_Manager(),
166 348 'ai' => new ThinkRank\AI\Manager(),
167 349 'frontend_seo' => new ThinkRank\Frontend\SEO_Manager(),
168 350 'seo_notice' => new ThinkRank\Admin\SEO_Notice(),
169 - 'integrations' => new ThinkRank\Integrations\Manager(),
351 + 'search_visibility_notice' => new ThinkRank\Admin\Search_Visibility_Notice(),
170 352 'performance_collector' => new ThinkRank\SEO\Performance_Data_Collector(),
353 + 'query_guard' => new ThinkRank\SEO\Query_Guard(),
354 + 'feeds' => new ThinkRank\SEO\Feed_Manager(),
355 + 'sitemap_stylesheet' => new ThinkRank\SEO\Sitemap_Stylesheet(),
356 + 'oembed' => new ThinkRank\SEO\Oembed_Manager(),
357 + 'content_visibility' => new ThinkRank\SEO\Content_Visibility(),
171 358 'instant_indexing' => new ThinkRank\SEO\Instant_Indexing_Manager(),
359 + 'instant_indexing_reconciler' => new ThinkRank\SEO\Instant_Indexing_Reconciler(),
172 360 'author_archives' => new ThinkRank\SEO\Author_Archives_Manager(),
361 + 'seo_analyzer' => new ThinkRank\SEO\SEO_Analyzer(),
362 + 'email_report' => new ThinkRank\SEO\Email_Report_Manager(),
363 + 'google_oauth' => new ThinkRank\Integrations\Google_OAuth_Proxy(),
364 + 'multilingual' => new ThinkRank\Integrations\Multilingual_Manager(),
365 + 'ai_traffic' => new ThinkRank\SEO\Ai_Traffic_Tracker(),
366 + 'analytics' => new ThinkRank\SEO\Analytics_Manager(),
367 + 'abilities' => new ThinkRank\Abilities\Abilities_Registrar(),
368 + 'mcp' => new ThinkRank\Mcp\Mcp_Manager(),
173 369 ];
174 370 }
175 371
176 372 /**
@@ -196,9 +392,9 @@
196 392 }
197 393
198 394 /**
199 395 * Get component instance
200 - *
396 + *
201 397 * @param string $component Component name
202 398 * @return object|null
203 399 */
204 400 public function get_component(string $component): ?object {
@@ -205,8 +401,45 @@
205 401 return $this->components[$component] ?? null;
206 402 }
207 403
208 404 /**
405 + * Shared Analytics Manager instance (lazy)
406 + *
407 + * @var ThinkRank\SEO\Analytics_Manager|null
408 + */
409 + private ?ThinkRank\SEO\Analytics_Manager $analytics_manager = null;
410 +
411 + /**
412 + * Get the shared Analytics Manager, with Google clients initialized.
413 + *
414 + * Consumers (including the Pro plugin, which probes for this accessor)
415 + * should use this instead of constructing their own Analytics_Manager:
416 + * each fresh construction re-reads/decrypts settings and re-initializes
417 + * the Google API clients.
418 + *
419 + * @since 1.18.0
420 + * @return ThinkRank\SEO\Analytics_Manager
421 + */
422 + public function get_analytics_manager(): ThinkRank\SEO\Analytics_Manager {
423 + if ($this->analytics_manager === null) {
424 + // Reuse the registered component. Constructing a second instance
425 + // here would work, but only the registered one has had init() run
426 + // on it, so the token-refresh cron would be scheduled against a
427 + // different object than the one callers actually use.
428 + $component = $this->components['analytics'] ?? null;
429 +
430 + $this->analytics_manager = $component instanceof ThinkRank\SEO\Analytics_Manager
431 + ? $component
432 + : new ThinkRank\SEO\Analytics_Manager();
433 +
434 + // init() defers initialize_clients() to the `init` hook; callers
435 + // that arrive earlier still need working clients.
436 + $this->analytics_manager->initialize_clients();
437 + }
438 + return $this->analytics_manager;
439 + }
440 +
441 + /**
209 442 * Plugin activation
210 443 *
211 444 * @return void
212 445 */
@@ -246,14 +479,27 @@
246 479
247 480 /**
248 481 * Handle errors consistently
249 482 *
483 + * Logs errors and surfaces them via _doing_it_wrong() when
484 + * WP_DEBUG is enabled, helping developers diagnose issues.
485 + *
250 486 * @param \Exception $e Exception to handle
251 487 * @return void
252 488 */
253 489 private function handle_error(\Exception $e): void {
254 - // Error handling without debug output
255 - error_log($e->getMessage());
490 + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
491 + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
492 + error_log('ThinkRank: ' . $e->getMessage());
493 + }
494 +
495 + if (defined('WP_DEBUG') && WP_DEBUG) {
496 + _doing_it_wrong(
497 + __METHOD__,
498 + esc_html($e->getMessage()),
499 + esc_html(THINKRANK_VERSION)
500 + );
501 + }
256 502 }
257 503
258 504 /**
259 505 * Get plugin version
@@ -287,8 +533,9 @@
287 533 * Initialize the plugin
288 534 *
289 535 * @return ThinkRank
290 536 */
537 +// phpcs:ignore Universal.Files.SeparateFunctionsFromOO.Mixed -- plugin bootstrap: the accessor belongs next to the class it returns.
291 538 function thinkrank(): ThinkRank {
292 539 return ThinkRank::get_instance();
293 540 }
294 541