PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.2.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.2.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 1.11.0 1.12.0 1.13.0 All 45 releases
thinkrank / thinkrank.php

thinkrank.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.2.0, at thinkrank.php

462 lines 14.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Plugin Name: ThinkRank
5 * Plugin URI: https://thinkrank.ai/
6 * Description: AI-native SEO plugin for WordPress. Automate and enhance your SEO with cutting-edge AI while maintaining editorial control.
7 * Version: 2.2.0
8 * Author: WPDeveloper
9 * Author URI: https://wpdeveloper.com/
10 * License: GPL v2 or later
11 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
12 * Text Domain: thinkrank
13 * Domain Path: /languages
14 * Requires at least: 6.0
15 * Requires PHP: 7.4
16 *
17 * @package ThinkRank
18 * @version 2.2.0
19 * @since 1.0.0
20 */
21
22 declare(strict_types=1);
23
24 // Prevent direct access
25 if (!defined('ABSPATH')) {
26 exit;
27 }
28
29 // Define plugin constants
30 define('THINKRANK_VERSION', '2.2.0');
31 define('THINKRANK_PLUGIN_FILE', __FILE__);
32 define('THINKRANK_PLUGIN_DIR', plugin_dir_path(__FILE__));
33 define('THINKRANK_PLUGIN_URL', plugin_dir_url(__FILE__));
34 define('THINKRANK_PLUGIN_BASENAME', plugin_basename(__FILE__));
35
36 // Minimum requirements check
37 if (version_compare(PHP_VERSION, '7.4', '<')) {
38 add_action('admin_notices', function () {
39 echo '<div class="notice notice-error"><p>';
40 echo esc_html__('ThinkRank requires PHP 7.4 or higher. Please upgrade your PHP version.', 'thinkrank');
41 echo '</p></div>';
42 });
43 return;
44 }
45
46 if (version_compare(get_bloginfo('version'), '6.0', '<')) {
47 add_action('admin_notices', function () {
48 echo '<div class="notice notice-error"><p>';
49 echo esc_html__('ThinkRank requires WordPress 6.0 or higher. Please upgrade your WordPress installation.', 'thinkrank');
50 echo '</p></div>';
51 });
52 return;
53 }
54
55 // Autoloader
56 require_once THINKRANK_PLUGIN_DIR . 'includes/class-autoloader.php';
57
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 /**
74 * Main ThinkRank Plugin Class
75 *
76 * Follows Single Responsibility Principle - only handles plugin initialization
77 *
78 * @since 1.0.0
79 */
80 final class ThinkRank {
81
82 /**
83 * Plugin instance (Singleton Pattern)
84 *
85 * @var ThinkRank|null
86 */
87 private static ?ThinkRank $instance = null;
88
89 /**
90 * Plugin components
91 *
92 * @var array
93 */
94 private array $components = [];
95
96 /**
97 * Get plugin instance (Singleton Pattern)
98 *
99 * @return ThinkRank
100 */
101 public static function get_instance(): ThinkRank {
102 if (null === self::$instance) {
103 self::$instance = new self();
104 }
105 return self::$instance;
106 }
107
108 /**
109 * Private constructor to prevent direct instantiation
110 */
111 private function __construct() {
112 $this->init_autoloader();
113 $this->init_hooks();
114 }
115
116 /**
117 * Prevent cloning
118 */
119 private function __clone() {
120 }
121
122 /**
123 * Prevent unserialization
124 *
125 * @throws \Exception On failure.
126 */
127 public function __wakeup() {
128 throw new \Exception('Cannot unserialize singleton');
129 }
130
131 /**
132 * Initialize autoloader
133 *
134 * @return void
135 */
136 private function init_autoloader(): void {
137 ThinkRank\Core\Autoloader::register();
138 }
139
140 /**
141 * Initialize WordPress hooks
142 *
143 * @return void
144 */
145 private function init_hooks(): void {
146 register_activation_hook(__FILE__, [$this, 'activate']);
147 register_deactivation_hook(__FILE__, [$this, 'deactivate']);
148
149 add_action('plugins_loaded', [$this, 'init']);
150 add_filter('plugin_action_links_' . THINKRANK_PLUGIN_BASENAME, [$this, 'add_action_links']);
151 }
152
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 /**
172 * Initialize plugin components
173 *
174 * @return void
175 */
176 public function init(): void {
177 try {
178 $this->maybe_update_database();
179 $this->register_sitemap_cron_listeners();
180 $this->register_brand_visibility_cron();
181 $this->load_components();
182 $this->init_components();
183 $this->load_template_functions();
184
185 do_action('thinkrank_loaded');
186 } catch (\Exception $e) {
187 $this->handle_error($e);
188 }
189 }
190
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
212 /**
213 * Register the Brand Visibility run-drain listener.
214 *
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.
219 *
220 * @return void
221 */
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 });
226
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 });
234 }
235
236 /**
237 * Check if database schema needs updating and run migrations
238 *
239 * Standard WordPress pattern: compare stored db_version against current,
240 * run dbDelta if stale. This handles schema changes (new tables, new columns)
241 * without requiring plugin deactivation/reactivation.
242 *
243 * @since 1.10.0
244 * @return void
245 */
246 private function maybe_update_database(): void {
247 $schema = new ThinkRank\Database\Database_Schema();
248 if ($schema->needs_update()) {
249 $schema->create_tables();
250 }
251 }
252
253 /**
254 * Load plugin components (Dependency Injection Container pattern)
255 *
256 * @return void
257 */
258 private function load_components(): void {
259 $this->components = [
260 'database' => new ThinkRank\Core\Database(),
261 'settings' => new ThinkRank\Core\Settings(),
262 'role_manager' => new ThinkRank\Core\Role_Manager(),
263 'security_headers' => new ThinkRank\Core\Security_Headers(),
264 'asset_optimizer' => new ThinkRank\Core\Asset_Optimizer(),
265 'usage_tracker' => new ThinkRank\Core\Usage_Tracker_Manager(),
266 'api' => new ThinkRank\API\Manager(),
267 'admin' => new ThinkRank\Admin\Manager(),
268 'blocks' => new ThinkRank\Editor\Blocks_Manager(),
269 'elementor' => new ThinkRank\Editor\Elementor_Manager(),
270 'ai' => new ThinkRank\AI\Manager(),
271 'frontend_seo' => new ThinkRank\Frontend\SEO_Manager(),
272 'seo_notice' => new ThinkRank\Admin\SEO_Notice(),
273 'search_visibility_notice' => new ThinkRank\Admin\Search_Visibility_Notice(),
274 'performance_collector' => new ThinkRank\SEO\Performance_Data_Collector(),
275 'instant_indexing' => new ThinkRank\SEO\Instant_Indexing_Manager(),
276 'instant_indexing_reconciler' => new ThinkRank\SEO\Instant_Indexing_Reconciler(),
277 'author_archives' => new ThinkRank\SEO\Author_Archives_Manager(),
278 'seo_analyzer' => new ThinkRank\SEO\SEO_Analyzer(),
279 'email_report' => new ThinkRank\SEO\Email_Report_Manager(),
280 'google_oauth' => new ThinkRank\Integrations\Google_OAuth_Proxy(),
281 'multilingual' => new ThinkRank\Integrations\Multilingual_Manager(),
282 'ai_traffic' => new ThinkRank\SEO\Ai_Traffic_Tracker(),
283 'auto_ai' => new ThinkRank\SEO\Auto_Ai_Optimizer(),
284 'analytics' => new ThinkRank\SEO\Analytics_Manager(),
285 'abilities' => new ThinkRank\Abilities\Abilities_Registrar(),
286 'mcp' => new ThinkRank\Mcp\Mcp_Manager(),
287 ];
288 }
289
290 /**
291 * Initialize all components
292 *
293 * @return void
294 */
295 private function init_components(): void {
296 foreach ($this->components as $component) {
297 if (method_exists($component, 'init')) {
298 $component->init();
299 }
300 }
301 }
302
303 /**
304 * Load template functions for themes
305 *
306 * @return void
307 */
308 private function load_template_functions(): void {
309 require_once THINKRANK_PLUGIN_DIR . 'includes/frontend/template-functions.php';
310 }
311
312 /**
313 * Get component instance
314 *
315 * @param string $component Component name
316 * @return object|null
317 */
318 public function get_component(string $component): ?object {
319 return $this->components[$component] ?? null;
320 }
321
322 /**
323 * Shared Analytics Manager instance (lazy)
324 *
325 * @var ThinkRank\SEO\Analytics_Manager|null
326 */
327 private ?ThinkRank\SEO\Analytics_Manager $analytics_manager = null;
328
329 /**
330 * Get the shared Analytics Manager, with Google clients initialized.
331 *
332 * Consumers (including the Pro plugin, which probes for this accessor)
333 * should use this instead of constructing their own Analytics_Manager:
334 * each fresh construction re-reads/decrypts settings and re-initializes
335 * the Google API clients.
336 *
337 * @since 1.18.0
338 * @return ThinkRank\SEO\Analytics_Manager
339 */
340 public function get_analytics_manager(): ThinkRank\SEO\Analytics_Manager {
341 if ($this->analytics_manager === null) {
342 // Reuse the registered component. Constructing a second instance
343 // here would work, but only the registered one has had init() run
344 // on it, so the token-refresh cron would be scheduled against a
345 // different object than the one callers actually use.
346 $component = $this->components['analytics'] ?? null;
347
348 $this->analytics_manager = $component instanceof ThinkRank\SEO\Analytics_Manager
349 ? $component
350 : new ThinkRank\SEO\Analytics_Manager();
351
352 // init() defers initialize_clients() to the `init` hook; callers
353 // that arrive earlier still need working clients.
354 $this->analytics_manager->initialize_clients();
355 }
356 return $this->analytics_manager;
357 }
358
359 /**
360 * Plugin activation
361 *
362 * @return void
363 */
364 public function activate(): void {
365 try {
366 $activator = new ThinkRank\Core\Activator();
367 $activator->activate();
368
369 // Flush rewrite rules
370 flush_rewrite_rules();
371 } catch (\Exception $e) {
372 $this->handle_error($e);
373 wp_die(
374 esc_html__('ThinkRank activation failed. Please check your server logs.', 'thinkrank'),
375 esc_html__('Plugin Activation Error', 'thinkrank'),
376 ['back_link' => true]
377 );
378 }
379 }
380
381 /**
382 * Plugin deactivation
383 *
384 * @return void
385 */
386 public function deactivate(): void {
387 try {
388 $deactivator = new ThinkRank\Core\Deactivator();
389 $deactivator->deactivate();
390
391 // Flush rewrite rules
392 flush_rewrite_rules();
393 } catch (\Exception $e) {
394 $this->handle_error($e);
395 }
396 }
397
398 /**
399 * Handle errors consistently
400 *
401 * Logs errors and surfaces them via _doing_it_wrong() when
402 * WP_DEBUG is enabled, helping developers diagnose issues.
403 *
404 * @param \Exception $e Exception to handle
405 * @return void
406 */
407 private function handle_error(\Exception $e): void {
408 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
409 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
410 error_log('ThinkRank: ' . $e->getMessage());
411 }
412
413 if (defined('WP_DEBUG') && WP_DEBUG) {
414 _doing_it_wrong(
415 __METHOD__,
416 esc_html($e->getMessage()),
417 esc_html(THINKRANK_VERSION)
418 );
419 }
420 }
421
422 /**
423 * Get plugin version
424 *
425 * @return string
426 */
427 public function get_version(): string {
428 return THINKRANK_VERSION;
429 }
430
431 /**
432 * Get plugin directory path
433 *
434 * @return string
435 */
436 public function get_plugin_dir(): string {
437 return THINKRANK_PLUGIN_DIR;
438 }
439
440 /**
441 * Get plugin URL
442 *
443 * @return string
444 */
445 public function get_plugin_url(): string {
446 return THINKRANK_PLUGIN_URL;
447 }
448 }
449
450 /**
451 * Initialize the plugin
452 *
453 * @return ThinkRank
454 */
455 // phpcs:ignore Universal.Files.SeparateFunctionsFromOO.Mixed -- plugin bootstrap: the accessor belongs next to the class it returns.
456 function thinkrank(): ThinkRank {
457 return ThinkRank::get_instance();
458 }
459
460 // Start the plugin
461 thinkrank();
462