PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.28.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.28.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
thinkrank / thinkrank.php

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

456 lines 14.1 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: 1.28.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: 8.0
16 *
17 * @package ThinkRank
18 * @version 1.28.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', '1.28.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, '8.0', '<')) {
38 add_action('admin_notices', function () {
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');
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 public function __wakeup() {
126 throw new \Exception('Cannot unserialize singleton');
127 }
128
129 /**
130 * Initialize autoloader
131 *
132 * @return void
133 */
134 private function init_autoloader(): void {
135 ThinkRank\Core\Autoloader::register();
136 }
137
138 /**
139 * Initialize WordPress hooks
140 *
141 * @return void
142 */
143 private function init_hooks(): void {
144 register_activation_hook(__FILE__, [$this, 'activate']);
145 register_deactivation_hook(__FILE__, [$this, 'deactivate']);
146
147 add_action('plugins_loaded', [$this, 'init']);
148 add_filter('plugin_action_links_' . THINKRANK_PLUGIN_BASENAME, [$this, 'add_action_links']);
149 }
150
151 /**
152 * Add a "Dashboard" link to the plugin action links on the Plugins page.
153 *
154 * @param array $links Existing plugin action links.
155 * @return array
156 */
157 public function add_action_links(array $links): array {
158 $dashboard_link = sprintf(
159 '<a href="%s">%s</a>',
160 esc_url(admin_url('admin.php?page=thinkrank')),
161 esc_html__('Dashboard', 'thinkrank')
162 );
163
164 array_unshift($links, $dashboard_link);
165
166 return $links;
167 }
168
169 /**
170 * Initialize plugin components
171 *
172 * @return void
173 */
174 public function init(): void {
175 try {
176 $this->maybe_update_database();
177 $this->register_sitemap_cron_listeners();
178 $this->register_brand_visibility_cron();
179 $this->load_components();
180 $this->init_components();
181 $this->load_template_functions();
182
183 do_action('thinkrank_loaded');
184 } catch (\Exception $e) {
185 $this->handle_error($e);
186 }
187 }
188
189 /**
190 * Register the sitemap regeneration WP-Cron listeners.
191 *
192 * Runs on plugins_loaded (via init()), so the callbacks exist on every
193 * request — including WP-Cron, which never fires rest_api_init and therefore
194 * never builds the Sitemap REST endpoint (whose constructor would otherwise
195 * be the only place the scheduled regeneration hooks get a listener). The
196 * generator is built lazily inside the callback so this stays cheap on the
197 * vast majority of requests where no regeneration is due.
198 *
199 * @return void
200 */
201 private function register_sitemap_cron_listeners(): void {
202 add_action('thinkrank_regenerate_sitemap', static function () {
203 (new ThinkRank\SEO\Sitemap_Generator())->auto_regenerate_sitemap();
204 });
205 add_action('thinkrank_regenerate_sitemap_settings', static function () {
206 (new ThinkRank\SEO\Sitemap_Generator())->regenerate_sitemap_from_settings();
207 });
208 }
209
210 /**
211 * Register the Brand Visibility run-drain listener.
212 *
213 * Runs on plugins_loaded (via init()) rather than from the REST endpoint,
214 * because the ticks that drain a run are WP-Cron requests — they never
215 * reach rest_api_init, so registering the listener there would mean a run
216 * starts and then never progresses.
217 *
218 * @return void
219 */
220 private function register_brand_visibility_cron(): void {
221 add_action(ThinkRank\AI\Brand_Visibility_Runner::TICK_HOOK, static function () {
222 (new ThinkRank\AI\Brand_Visibility_Runner())->tick();
223 });
224
225 // Safety net: a tick killed by a fatal or a worker timeout never
226 // reaches its own reschedule, which would strand the run. The
227 // watchdog re-arms the drain and unschedules itself when idle.
228 add_filter('cron_schedules', [ThinkRank\AI\Brand_Visibility_Runner::class, 'add_cron_interval']); // phpcs:ignore WordPress.WP.CronInterval.ChangeDetected
229 add_action(ThinkRank\AI\Brand_Visibility_Runner::WATCHDOG_HOOK, static function () {
230 (new ThinkRank\AI\Brand_Visibility_Runner())->watchdog();
231 });
232 }
233
234 /**
235 * Check if database schema needs updating and run migrations
236 *
237 * Standard WordPress pattern: compare stored db_version against current,
238 * run dbDelta if stale. This handles schema changes (new tables, new columns)
239 * without requiring plugin deactivation/reactivation.
240 *
241 * @since 1.10.0
242 * @return void
243 */
244 private function maybe_update_database(): void {
245 $schema = new ThinkRank\Database\Database_Schema();
246 if ($schema->needs_update()) {
247 $schema->create_tables();
248 }
249 }
250
251 /**
252 * Load plugin components (Dependency Injection Container pattern)
253 *
254 * @return void
255 */
256 private function load_components(): void {
257 $this->components = [
258 'database' => new ThinkRank\Core\Database(),
259 'settings' => new ThinkRank\Core\Settings(),
260 'role_manager' => new ThinkRank\Core\Role_Manager(),
261 'security_headers' => new ThinkRank\Core\Security_Headers(),
262 'asset_optimizer' => new ThinkRank\Core\Asset_Optimizer(),
263 'usage_tracker' => new ThinkRank\Core\Usage_Tracker_Manager(),
264 'api' => new ThinkRank\API\Manager(),
265 'admin' => new ThinkRank\Admin\Manager(),
266 'blocks' => new ThinkRank\Editor\Blocks_Manager(),
267 'elementor' => new ThinkRank\Editor\Elementor_Manager(),
268 'ai' => new ThinkRank\AI\Manager(),
269 'frontend_seo' => new ThinkRank\Frontend\SEO_Manager(),
270 'seo_notice' => new ThinkRank\Admin\SEO_Notice(),
271 'performance_collector' => new ThinkRank\SEO\Performance_Data_Collector(),
272 'instant_indexing' => new ThinkRank\SEO\Instant_Indexing_Manager(),
273 'author_archives' => new ThinkRank\SEO\Author_Archives_Manager(),
274 'email_report' => new ThinkRank\SEO\Email_Report_Manager(),
275 'google_oauth' => new ThinkRank\Integrations\Google_OAuth_Proxy(),
276 'multilingual' => new ThinkRank\Integrations\Multilingual_Manager(),
277 'ai_traffic' => new ThinkRank\SEO\Ai_Traffic_Tracker(),
278 'auto_ai' => new ThinkRank\SEO\Auto_Ai_Optimizer(),
279 'analytics' => new ThinkRank\SEO\Analytics_Manager(),
280 'abilities' => new ThinkRank\Abilities\Abilities_Registrar(),
281 'mcp' => new ThinkRank\Mcp\Mcp_Manager(),
282 ];
283 }
284
285 /**
286 * Initialize all components
287 *
288 * @return void
289 */
290 private function init_components(): void {
291 foreach ($this->components as $component) {
292 if (method_exists($component, 'init')) {
293 $component->init();
294 }
295 }
296 }
297
298 /**
299 * Load template functions for themes
300 *
301 * @return void
302 */
303 private function load_template_functions(): void {
304 require_once THINKRANK_PLUGIN_DIR . 'includes/frontend/template-functions.php';
305 }
306
307 /**
308 * Get component instance
309 *
310 * @param string $component Component name
311 * @return object|null
312 */
313 public function get_component(string $component): ?object {
314 return $this->components[$component] ?? null;
315 }
316
317 /**
318 * Shared Analytics Manager instance (lazy)
319 *
320 * @var ThinkRank\SEO\Analytics_Manager|null
321 */
322 private ?ThinkRank\SEO\Analytics_Manager $analytics_manager = null;
323
324 /**
325 * Get the shared Analytics Manager, with Google clients initialized.
326 *
327 * Consumers (including the Pro plugin, which probes for this accessor)
328 * should use this instead of constructing their own Analytics_Manager:
329 * each fresh construction re-reads/decrypts settings and re-initializes
330 * the Google API clients.
331 *
332 * @since 1.18.0
333 * @return ThinkRank\SEO\Analytics_Manager
334 */
335 public function get_analytics_manager(): ThinkRank\SEO\Analytics_Manager {
336 if ($this->analytics_manager === null) {
337 // Reuse the registered component. Constructing a second instance
338 // here would work, but only the registered one has had init() run
339 // on it, so the token-refresh cron would be scheduled against a
340 // different object than the one callers actually use.
341 $component = $this->components['analytics'] ?? null;
342
343 $this->analytics_manager = $component instanceof ThinkRank\SEO\Analytics_Manager
344 ? $component
345 : new ThinkRank\SEO\Analytics_Manager();
346
347 // init() defers initialize_clients() to the `init` hook; callers
348 // that arrive earlier still need working clients.
349 $this->analytics_manager->initialize_clients();
350 }
351 return $this->analytics_manager;
352 }
353
354 /**
355 * Plugin activation
356 *
357 * @return void
358 */
359 public function activate(): void {
360 try {
361 $activator = new ThinkRank\Core\Activator();
362 $activator->activate();
363
364 // Flush rewrite rules
365 flush_rewrite_rules();
366 } catch (\Exception $e) {
367 $this->handle_error($e);
368 wp_die(
369 esc_html__('ThinkRank activation failed. Please check your server logs.', 'thinkrank'),
370 esc_html__('Plugin Activation Error', 'thinkrank'),
371 ['back_link' => true]
372 );
373 }
374 }
375
376 /**
377 * Plugin deactivation
378 *
379 * @return void
380 */
381 public function deactivate(): void {
382 try {
383 $deactivator = new ThinkRank\Core\Deactivator();
384 $deactivator->deactivate();
385
386 // Flush rewrite rules
387 flush_rewrite_rules();
388 } catch (\Exception $e) {
389 $this->handle_error($e);
390 }
391 }
392
393 /**
394 * Handle errors consistently
395 *
396 * Logs errors and surfaces them via _doing_it_wrong() when
397 * WP_DEBUG is enabled, helping developers diagnose issues.
398 *
399 * @param \Exception $e Exception to handle
400 * @return void
401 */
402 private function handle_error(\Exception $e): void {
403 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
404 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
405 error_log('ThinkRank: ' . $e->getMessage());
406 }
407
408 if (defined('WP_DEBUG') && WP_DEBUG) {
409 _doing_it_wrong(
410 __METHOD__,
411 esc_html($e->getMessage()),
412 esc_html(THINKRANK_VERSION)
413 );
414 }
415 }
416
417 /**
418 * Get plugin version
419 *
420 * @return string
421 */
422 public function get_version(): string {
423 return THINKRANK_VERSION;
424 }
425
426 /**
427 * Get plugin directory path
428 *
429 * @return string
430 */
431 public function get_plugin_dir(): string {
432 return THINKRANK_PLUGIN_DIR;
433 }
434
435 /**
436 * Get plugin URL
437 *
438 * @return string
439 */
440 public function get_plugin_url(): string {
441 return THINKRANK_PLUGIN_URL;
442 }
443 }
444
445 /**
446 * Initialize the plugin
447 *
448 * @return ThinkRank
449 */
450 function thinkrank(): ThinkRank {
451 return ThinkRank::get_instance();
452 }
453
454 // Start the plugin
455 thinkrank();
456