| @@ -1,22 +1,22 @@ | ||
| 1 | 1 | <?php |
| 2 | + | |
| 2 | 3 | /** |
| 3 | 4 | * Plugin Name: ThinkRank |
| 4 | - * Plugin URI: https://thinkrank.ai/wordpress-plugin | |
| 5 | + * Plugin URI: https://thinkrank.ai/ | |
| 5 | 6 | * Description: AI-native SEO plugin for WordPress. Automate and enhance your SEO with cutting-edge AI while maintaining editorial control. |
| 6 | - * Version: 1.1.0 | |
| 7 | - * Author: rupok | |
| 8 | - * Author URI: https://thinkrank.ai | |
| 7 | + * Version: 2.7.0 | |
| 8 | + * Author: WPDeveloper | |
| 9 | + * Author URI: https://wpdeveloper.com/ | |
| 9 | 10 | * License: GPL v2 or later |
| 10 | 11 | * License URI: https://www.gnu.org/licenses/gpl-2.0.html |
| 11 | 12 | * Text Domain: thinkrank |
| 12 | 13 | * Domain Path: /languages |
| 13 | 14 | * Requires at least: 6.0 |
| 14 | - * Tested up to: 6.8 | |
| 15 | - * Requires PHP: 8.0 | |
| 15 | + * Requires PHP: 7.4 | |
| 16 | 16 | * |
| 17 | 17 | * @package ThinkRank |
| 18 | - * @version 1.1.0 | |
| 18 | + * @version 2.7.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.1.0'); | |
| 30 | +define('THINKRANK_VERSION', '2.7.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', '<')) { | |
| 38 | - add_action('admin_notices', function() { | |
| 37 | +if (version_compare(PHP_VERSION, '7.4', '<')) { | |
| 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 | } |
| @@ -43,9 +43,9 @@ | ||
| 43 | 43 | return; |
| 44 | 44 | } |
| 45 | 45 | |
| 46 | 46 | if (version_compare(get_bloginfo('version'), '6.0', '<')) { |
| 47 | - add_action('admin_notices', function() { | |
| 47 | + add_action('admin_notices', function () { | |
| 48 | 48 | echo '<div class="notice notice-error"><p>'; |
| 49 | 49 | echo esc_html__('ThinkRank requires WordPress 6.0 or higher. Please upgrade your WordPress installation.', 'thinkrank'); |
| 50 | 50 | echo '</p></div>'; |
| 51 | 51 | }); |
| @@ -55,8 +55,23 @@ | ||
| 55 | 55 | // Autoloader |
| 56 | 56 | require_once THINKRANK_PLUGIN_DIR . 'includes/class-autoloader.php'; |
| 57 | 57 | |
| 58 | 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 | +/** | |
| 59 | 74 | * Main ThinkRank Plugin Class |
| 60 | 75 | * |
| 61 | 76 | * Follows Single Responsibility Principle - only handles plugin initialization |
| 62 | 77 | * |
| @@ -62,9 +77,9 @@ | ||
| 62 | 77 | * |
| 63 | 78 | * @since 1.0.0 |
| 64 | 79 | */ |
| 65 | 80 | final class ThinkRank { |
| 66 | - | |
| 81 | + | |
| 67 | 82 | /** |
| 68 | 83 | * Plugin instance (Singleton Pattern) |
| 69 | 84 | * |
| 70 | 85 | * @var ThinkRank|null |
| @@ -69,9 +84,9 @@ | ||
| 69 | 84 | * |
| 70 | 85 | * @var ThinkRank|null |
| 71 | 86 | */ |
| 72 | 87 | private static ?ThinkRank $instance = null; |
| 73 | - | |
| 88 | + | |
| 74 | 89 | /** |
| 75 | 90 | * Plugin components |
| 76 | 91 | * |
| 77 | 92 | * @var array |
| @@ -76,9 +91,9 @@ | ||
| 76 | 91 | * |
| 77 | 92 | * @var array |
| 78 | 93 | */ |
| 79 | 94 | private array $components = []; |
| 80 | - | |
| 95 | + | |
| 81 | 96 | /** |
| 82 | 97 | * Get plugin instance (Singleton Pattern) |
| 83 | 98 | * |
| 84 | 99 | * @return ThinkRank |
| @@ -88,9 +103,9 @@ | ||
| 88 | 103 | self::$instance = new self(); |
| 89 | 104 | } |
| 90 | 105 | return self::$instance; |
| 91 | 106 | } |
| 92 | - | |
| 107 | + | |
| 93 | 108 | /** |
| 94 | 109 | * Private constructor to prevent direct instantiation |
| 95 | 110 | */ |
| 96 | 111 | private function __construct() { |
| @@ -96,21 +111,24 @@ | ||
| 96 | 111 | private function __construct() { |
| 97 | 112 | $this->init_autoloader(); |
| 98 | 113 | $this->init_hooks(); |
| 99 | 114 | } |
| 100 | - | |
| 115 | + | |
| 101 | 116 | /** |
| 102 | 117 | * Prevent cloning |
| 103 | 118 | */ |
| 104 | - private function __clone() {} | |
| 105 | - | |
| 119 | + private function __clone() { | |
| 120 | + } | |
| 121 | + | |
| 106 | 122 | /** |
| 107 | 123 | * Prevent unserialization |
| 124 | + * | |
| 125 | + * @throws \Exception On failure. | |
| 108 | 126 | */ |
| 109 | 127 | public function __wakeup() { |
| 110 | 128 | throw new \Exception('Cannot unserialize singleton'); |
| 111 | 129 | } |
| 112 | - | |
| 130 | + | |
| 113 | 131 | /** |
| 114 | 132 | * Initialize autoloader |
| 115 | 133 | * |
| 116 | 134 | * @return void |
| @@ -117,9 +135,9 @@ | ||
| 117 | 135 | */ |
| 118 | 136 | private function init_autoloader(): void { |
| 119 | 137 | ThinkRank\Core\Autoloader::register(); |
| 120 | 138 | } |
| 121 | - | |
| 139 | + | |
| 122 | 140 | /** |
| 123 | 141 | * Initialize WordPress hooks |
| 124 | 142 | * |
| 125 | 143 | * @return void |
| @@ -128,11 +146,30 @@ | ||
| 128 | 146 | register_activation_hook(__FILE__, [$this, 'activate']); |
| 129 | 147 | register_deactivation_hook(__FILE__, [$this, 'deactivate']); |
| 130 | 148 | |
| 131 | 149 | add_action('plugins_loaded', [$this, 'init']); |
| 150 | + add_filter('plugin_action_links_' . THINKRANK_PLUGIN_BASENAME, [$this, 'add_action_links']); | |
| 132 | 151 | } |
| 133 | - | |
| 152 | + | |
| 134 | 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 | + /** | |
| 135 | 172 | * Initialize plugin components |
| 136 | 173 | * |
| 137 | 174 | * @return void |
| 138 | 175 | */ |
| @@ -137,8 +174,11 @@ | ||
| 137 | 174 | * @return void |
| 138 | 175 | */ |
| 139 | 176 | public function init(): void { |
| 140 | 177 | try { |
| 178 | + $this->maybe_update_database(); | |
| 179 | + $this->register_sitemap_cron_listeners(); | |
| 180 | + $this->clear_retired_brand_visibility_cron(); | |
| 141 | 181 | $this->load_components(); |
| 142 | 182 | $this->init_components(); |
| 143 | 183 | $this->load_template_functions(); |
| 144 | 184 | |
| @@ -146,10 +186,148 @@ | ||
| 146 | 186 | } catch (\Exception $e) { |
| 147 | 187 | $this->handle_error($e); |
| 148 | 188 | } |
| 149 | 189 | } |
| 150 | - | |
| 190 | + | |
| 151 | 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 | + /** | |
| 152 | 330 | * Load plugin components (Dependency Injection Container pattern) |
| 153 | 331 | * |
| 154 | 332 | * @return void |
| 155 | 333 | */ |
| @@ -154,22 +332,44 @@ | ||
| 154 | 332 | * @return void |
| 155 | 333 | */ |
| 156 | 334 | private function load_components(): void { |
| 157 | 335 | $this->components = [ |
| 158 | - 'database' => new ThinkRank\Core\Database(), | |
| 159 | - 'settings' => new ThinkRank\Core\Settings(), | |
| 336 | + 'database' => new ThinkRank\Core\Database(), | |
| 337 | + 'settings' => new ThinkRank\Core\Settings(), | |
| 338 | + 'role_manager' => new ThinkRank\Core\Role_Manager(), | |
| 160 | 339 | 'security_headers' => new ThinkRank\Core\Security_Headers(), |
| 161 | 340 | 'asset_optimizer' => new ThinkRank\Core\Asset_Optimizer(), |
| 162 | - 'api' => new ThinkRank\API\Manager(), | |
| 163 | - 'admin' => new ThinkRank\Admin\Manager(), | |
| 164 | - 'ai' => new ThinkRank\AI\Manager(), | |
| 341 | + 'usage_tracker' => new ThinkRank\Core\Usage_Tracker_Manager(), | |
| 342 | + 'api' => new ThinkRank\API\Manager(), | |
| 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(), | |
| 348 | + 'ai' => new ThinkRank\AI\Manager(), | |
| 165 | 349 | 'frontend_seo' => new ThinkRank\Frontend\SEO_Manager(), |
| 166 | - 'seo_notice' => new ThinkRank\Admin\SEO_Notice(), | |
| 167 | - 'integrations' => new ThinkRank\Integrations\Manager(), | |
| 350 | + 'seo_notice' => new ThinkRank\Admin\SEO_Notice(), | |
| 351 | + 'search_visibility_notice' => new ThinkRank\Admin\Search_Visibility_Notice(), | |
| 168 | 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(), | |
| 358 | + 'instant_indexing' => new ThinkRank\SEO\Instant_Indexing_Manager(), | |
| 359 | + 'instant_indexing_reconciler' => new ThinkRank\SEO\Instant_Indexing_Reconciler(), | |
| 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(), | |
| 169 | 369 | ]; |
| 170 | 370 | } |
| 171 | - | |
| 371 | + | |
| 172 | 372 | /** |
| 173 | 373 | * Initialize all components |
| 174 | 374 | * |
| 175 | 375 | * @return void |
| @@ -189,12 +389,12 @@ | ||
| 189 | 389 | */ |
| 190 | 390 | private function load_template_functions(): void { |
| 191 | 391 | require_once THINKRANK_PLUGIN_DIR . 'includes/frontend/template-functions.php'; |
| 192 | 392 | } |
| 193 | - | |
| 393 | + | |
| 194 | 394 | /** |
| 195 | 395 | * Get component instance |
| 196 | - * | |
| 396 | + * | |
| 197 | 397 | * @param string $component Component name |
| 198 | 398 | * @return object|null |
| 199 | 399 | */ |
| 200 | 400 | public function get_component(string $component): ?object { |
| @@ -199,10 +399,47 @@ | ||
| 199 | 399 | */ |
| 200 | 400 | public function get_component(string $component): ?object { |
| 201 | 401 | return $this->components[$component] ?? null; |
| 202 | 402 | } |
| 203 | - | |
| 403 | + | |
| 204 | 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 | + /** | |
| 205 | 442 | * Plugin activation |
| 206 | 443 | * |
| 207 | 444 | * @return void |
| 208 | 445 | */ |
| @@ -209,12 +446,11 @@ | ||
| 209 | 446 | public function activate(): void { |
| 210 | 447 | try { |
| 211 | 448 | $activator = new ThinkRank\Core\Activator(); |
| 212 | 449 | $activator->activate(); |
| 213 | - | |
| 450 | + | |
| 214 | 451 | // Flush rewrite rules |
| 215 | 452 | flush_rewrite_rules(); |
| 216 | - | |
| 217 | 453 | } catch (\Exception $e) { |
| 218 | 454 | $this->handle_error($e); |
| 219 | 455 | wp_die( |
| 220 | 456 | esc_html__('ThinkRank activation failed. Please check your server logs.', 'thinkrank'), |
| @@ -222,9 +458,9 @@ | ||
| 222 | 458 | ['back_link' => true] |
| 223 | 459 | ); |
| 224 | 460 | } |
| 225 | 461 | } |
| 226 | - | |
| 462 | + | |
| 227 | 463 | /** |
| 228 | 464 | * Plugin deactivation |
| 229 | 465 | * |
| 230 | 466 | * @return void |
| @@ -232,27 +468,40 @@ | ||
| 232 | 468 | public function deactivate(): void { |
| 233 | 469 | try { |
| 234 | 470 | $deactivator = new ThinkRank\Core\Deactivator(); |
| 235 | 471 | $deactivator->deactivate(); |
| 236 | - | |
| 472 | + | |
| 237 | 473 | // Flush rewrite rules |
| 238 | 474 | flush_rewrite_rules(); |
| 239 | - | |
| 240 | 475 | } catch (\Exception $e) { |
| 241 | 476 | $this->handle_error($e); |
| 242 | 477 | } |
| 243 | 478 | } |
| 244 | - | |
| 479 | + | |
| 245 | 480 | /** |
| 246 | 481 | * Handle errors consistently |
| 247 | 482 | * |
| 483 | + * Logs errors and surfaces them via _doing_it_wrong() when | |
| 484 | + * WP_DEBUG is enabled, helping developers diagnose issues. | |
| 485 | + * | |
| 248 | 486 | * @param \Exception $e Exception to handle |
| 249 | 487 | * @return void |
| 250 | 488 | */ |
| 251 | 489 | private function handle_error(\Exception $e): void { |
| 252 | - // Error handling without debug output | |
| 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 | + } | |
| 253 | 502 | } |
| 254 | - | |
| 503 | + | |
| 255 | 504 | /** |
| 256 | 505 | * Get plugin version |
| 257 | 506 | * |
| 258 | 507 | * @return string |
| @@ -259,9 +508,9 @@ | ||
| 259 | 508 | */ |
| 260 | 509 | public function get_version(): string { |
| 261 | 510 | return THINKRANK_VERSION; |
| 262 | 511 | } |
| 263 | - | |
| 512 | + | |
| 264 | 513 | /** |
| 265 | 514 | * Get plugin directory path |
| 266 | 515 | * |
| 267 | 516 | * @return string |
| @@ -268,9 +517,9 @@ | ||
| 268 | 517 | */ |
| 269 | 518 | public function get_plugin_dir(): string { |
| 270 | 519 | return THINKRANK_PLUGIN_DIR; |
| 271 | 520 | } |
| 272 | - | |
| 521 | + | |
| 273 | 522 | /** |
| 274 | 523 | * Get plugin URL |
| 275 | 524 | * |
| 276 | 525 | * @return string |
| @@ -284,8 +533,9 @@ | ||
| 284 | 533 | * Initialize the plugin |
| 285 | 534 | * |
| 286 | 535 | * @return ThinkRank |
| 287 | 536 | */ |
| 537 | +// phpcs:ignore Universal.Files.SeparateFunctionsFromOO.Mixed -- plugin bootstrap: the accessor belongs next to the class it returns. | |
| 288 | 538 | function thinkrank(): ThinkRank { |
| 289 | 539 | return ThinkRank::get_instance(); |
| 290 | 540 | } |
| 291 | 541 | |