PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.11
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.11
2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.5.23 2.5.24 All 137 releases
metasync / metasync.php

metasync.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.11, at metasync.php

935 lines 32.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * This file is read by WordPress to generate the plugin information in the plugin
5 * admin area. This file also includes all of the dependencies used by the plugin,
6 * registers the activation and deactivation functions, and defines a function
7 * that starts the plugin.
8 *
9 * @package Search Atlas SEO
10 * @copyright Copyright (C) 2021-2025, Search Atlas Group - support@searchatlas.com
11 * @link https://searchatlas.com/
12 * @since 1.0.0
13 *
14 * @wordpress-plugin
15 * Plugin Name: Search Atlas: The Premier AI SEO Plugin for Instant Optimization
16 * Plugin URI: https://searchatlas.com/
17 * Description: Search Atlas SEO is an intuitive WordPress Plugin that transforms the most complicated, most labor-intensive SEO tasks into streamlined, straightforward processes. With a few clicks, the meta-bulk update feature automates the re-optimization of meta tags using AI to increase clicks. Stay up-to-date with the freshest Google Search data for your entire site or targeted URLs within the Meta Sync plug-in page.
18 * Version: 2.6.11
19 * Author: Search Atlas
20 * Author URI: https://searchatlas.com
21 * License: GPL v3
22 * License URI: https://www.gnu.org/licenses/gpl-3.0.txt
23 * Text Domain: metasync
24 */
25
26 // If this file is called directly, abort.
27 if (!defined('WPINC')) {
28 die;
29 }
30
31 // Composer classmap autoloader — loads all plugin classes on demand.
32 require_once __DIR__ . '/vendor/autoload.php';
33
34 /**
35 * Currently plugin version.
36 * Start at version 1.0.0 and use SemVer - https://semver.org
37 * Rename this for your plugin and update it as you release new versions.
38 */
39 $metasync_version = '2.6.11';
40 define('METASYNC_VERSION', preg_match('/^\d+\.\d+/', $metasync_version) ? $metasync_version : '9.9.9');
41 /**
42 * Define the current required php version
43 * This will be used to validate whether the user can install the plugin or not
44 */
45 define('METASYNC_MIN_PHP', '8.2');
46
47 /**
48 * Define the current required php version
49 * This will be used to validate whether the user can install the plugin or not
50 */
51 define('METASYNC_MIN_WP', '5.2');
52
53 /**
54 * Telemetry Configuration Constants
55 * These replace the old database options for better security and consistency
56 */
57 define('METASYNC_SENTRY_PROJECT_ID', '4509950439849985');
58 define('METASYNC_SENTRY_ENVIRONMENT', 'production');
59 define('METASYNC_SENTRY_RELEASE', METASYNC_VERSION);
60 define('METASYNC_SENTRY_SAMPLE_RATE', 1.0);
61
62 /**
63 * GA4 Analytics Configuration
64 * Measurement ID for Google Analytics 4 event tracking (format: G-XXXXXXXX)
65 *
66 * IMPORTANT: This constant is REQUIRED for GA4 tracking to function.
67 * If not defined or empty, all GA4 analytics tracking will be disabled.
68 *
69 * GA4_API_SECRET is required for server-side Measurement Protocol events
70 * (Content Genius, OTTO optimization). Generate it in GA4:
71 * Admin → Data Streams → (stream) → Measurement Protocol → Create
72 */
73 define('METASYNC_GA4_MEASUREMENT_ID', 'G-SBLWW1EMTJ');
74 define('METASYNC_GA4_API_SECRET', 'nMGs22mxQ3qVUy-aInqfZA');
75
76 /**
77 * Define whether to show the plugin status in WordPress admin top navigation bar
78 * Set to false to hide the status indicator
79 */
80 define('METASYNC_SHOW_ADMIN_BAR_STATUS', true);
81
82 /**
83 * Sanitize POST/GET/REQUEST data recursively
84 *
85 * @param array $data Data to sanitize
86 * @return array Sanitized data
87 */
88 if (!function_exists('metasync_sanitize_input_array')) {
89 function metasync_sanitize_input_array($data) {
90 if (!is_array($data)) {
91 return sanitize_text_field($data);
92 }
93
94 $sanitized = [];
95 foreach ($data as $key => $value) {
96 if (is_array($value)) {
97 $sanitized[$key] = metasync_sanitize_input_array($value);
98 } else {
99 // Check if it's a URL
100 if (filter_var($value, FILTER_VALIDATE_URL)) {
101 $sanitized[$key] = esc_url_raw($value);
102 } else {
103 $sanitized[$key] = sanitize_text_field($value);
104 }
105 }
106 }
107 return $sanitized;
108 }
109 }
110
111 // Skip heavy MetaSync init on admin-ajax requests that don't target our own actions (Sentry issue 7441226449 / WP-227).
112 if (!function_exists('metasync_is_non_metasync_admin_ajax')) {
113 function metasync_is_non_metasync_admin_ajax() {
114 static $result = null;
115 if ($result !== null) {
116 return $result;
117 }
118 if (!defined('DOING_AJAX') || !DOING_AJAX) {
119 return ($result = false);
120 }
121 $action = isset($_POST['action']) ? sanitize_text_field(wp_unslash($_POST['action']))
122 : (isset($_GET['action']) ? sanitize_text_field(wp_unslash($_GET['action'])) : '');
123 if ($action === '') {
124 return ($result = true);
125 }
126 if (strpos($action, 'metasync_') === 0 || strpos($action, 'meta_sync_') === 0 || $action === 'sample-permalink') {
127 return ($result = false);
128 }
129 return ($result = true);
130 }
131 }
132
133 // Lazy-load guard: only initialise the MCP server when the request is actually targeting
134 // the MCP REST route (or its sibling SEO-inventory route, which depends on $metasync_mcp_server
135 // for its permission callback). Saves ~2-5 MB / 10-50 ms on the 99.9% of requests that never
136 // touch MCP. See WP-255.
137 if (!function_exists('metasync_is_mcp_rest_request')) {
138 function metasync_is_mcp_rest_request(): bool {
139 $uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '';
140 if ($uri === '') {
141 return false;
142 }
143 $prefix = function_exists('rest_get_url_prefix') ? rest_get_url_prefix() : 'wp-json';
144 $needles = [
145 '/' . $prefix . '/metasync/v1/mcp',
146 '/' . $prefix . '/metasync/v1/seo-inventory',
147 'rest_route=/metasync/v1/mcp',
148 'rest_route=/metasync/v1/seo-inventory',
149 'rest_route=%2Fmetasync%2Fv1%2Fmcp',
150 'rest_route=%2Fmetasync%2Fv1%2Fseo-inventory',
151 ];
152 foreach ($needles as $needle) {
153 if (stripos($uri, $needle) !== false) {
154 return true;
155 }
156 }
157 return false;
158 }
159 }
160
161 // Phase 2 — these files have file-level side effects (add_action outside class body)
162 // and must remain as explicit require_once until refactored.
163 require_once plugin_dir_path( __FILE__ ) . 'includes/class-metasync-api-backoff-rest.php';
164 require_once plugin_dir_path( __FILE__ ) . 'includes/class-metasync-error-logger.php';
165
166 /**
167 * Include the Otto Pixel Php Code
168 */
169 if (!metasync_is_non_metasync_admin_ajax()) {
170 require_once plugin_dir_path( __FILE__ ) . '/otto/otto_pixel.php';
171 require_once plugin_dir_path( __FILE__ ) . '/otto/class-metasync-otto-clone-meta-cleaner.php';
172 Metasync_Otto_Clone_Meta_Cleaner::register();
173 }
174
175
176 /**
177 * Initialize OTTO Persistence Settings (registers REST API endpoints)
178 */
179 add_action('init', function() {
180 if (metasync_is_non_metasync_admin_ajax()) {
181 return;
182 }
183 Metasync_Otto_Persistence_Settings::init();
184 }, 5);
185
186 /**
187 * The code that runs during plugin activation.
188 * This action is documented in includes/class-metasync-activator.php
189 */
190 function activate_metasync()
191 {
192 # Include WordPress plugin functions to access plugin metadata
193 if (!function_exists('get_plugin_data')) {
194 require_once ABSPATH . 'wp-admin/includes/plugin.php';
195 }
196
197 # Get plugin data
198 $plugin_data = get_plugin_data(__FILE__);
199
200 # Get the plugin name
201 $plugin_name = $plugin_data['Name'];
202
203 # Get the current WordPress
204 global $wp_version;
205
206 # Get the current php version
207 $php_version = PHP_VERSION;
208
209 # Check for incompatible WordPress version
210 if (version_compare($wp_version, METASYNC_MIN_WP, '<')) {
211
212 #show error
213 wp_die(
214
215 #craft the message
216 sprintf(
217 '%s requires WordPress version %s or later. You are currently using version %s. Please update WordPress to activate this plugin.',
218 esc_html($plugin_name),
219 METASYNC_MIN_WP,
220 esc_html($wp_version)
221 ),
222
223 #the plugin title as page title
224 esc_html($plugin_name).'Plugin Activation Error',
225
226 #include the back link
227 ['back_link' => true]
228 );
229 }
230
231 # Check for incompatible PHP version
232 if (version_compare($php_version, METASYNC_MIN_PHP, '<')) {
233
234 #show error message
235 wp_die(
236
237 #craft the message
238 sprintf(
239 '%s requires PHP version %s or later. You are currently using version %s. Please update PHP to activate this plugin.',
240 esc_html($plugin_name),
241 METASYNC_MIN_PHP,
242 esc_html($php_version)
243 ),
244
245 #the plugin title as page title
246 esc_html($plugin_name).'Plugin Activation Error',
247
248 #include the back link
249 ['back_link' => true]
250 );
251 }
252
253 Metasync_Activator::activate();
254 // class name is changed at class-db-migrations.php
255 MetaSync_DBMigration::activation();
256
257 // Set initial version
258 update_option('metasync_version', METASYNC_VERSION);
259
260 // WP-315: Record activation timestamp so Divi CSS fix transients
261 // auto-invalidate after plugin deactivate/reactivate cycles.
262 update_option('metasync_activated_at', (string) time());
263
264 // Clear all cache plugins to ensure fresh start
265 Metasync_Cache_Purge::purge_all('plugin_activation');
266
267 // WP-332: Migrate physical sitemap files on activation.
268 metasync_migrate_physical_sitemaps();
269 }
270
271 // Log-sync removed - error monitoring now handled by Sentry
272 // require_once plugin_dir_path(__FILE__) . 'log-sync/log-sync.php';
273
274 /**
275 * Initialize telemetry system for error monitoring and Sentry integration
276 */
277 if (!metasync_is_non_metasync_admin_ajax()) {
278 require_once plugin_dir_path(__FILE__) . 'telemetry/telemetry-init.php';
279 }
280
281 /**
282 * The code that runs during plugin deactivation.
283 * This action is documented in includes/class-metasync-deactivator.php
284 */
285 function deactivate_metasync()
286 {
287 Metasync_Deactivator::deactivate();
288 // class name is changed at class-db-migrations.php
289 MetaSync_DBMigration::deactivation();
290
291 // Clear news/video sitemap caches
292 delete_transient('metasync_vsm_' . md5('news-sitemap.xml'));
293 delete_transient('metasync_vsm_' . md5('video-sitemap.xml'));
294 delete_option('metasync_sitemap_virtual_index');
295
296 // Clear OTTO JS detection cache so re-activation gets a fresh check
297 delete_transient('metasync_otto_js_detected');
298 }
299
300 register_activation_hook(__FILE__, 'activate_metasync');
301 register_deactivation_hook(__FILE__, 'deactivate_metasync');
302
303 /**
304 * WP-332: Migrate physical sitemap .xml files into transients and delete them.
305 *
306 * Physical files in ABSPATH cause nginx/Plesk to 403 before WordPress can
307 * serve them. Called from both the activation hook and the version-gate
308 * migration so it covers fresh activations and auto-updates.
309 */
310 function metasync_migrate_physical_sitemaps()
311 {
312 update_option('metasync_sitemap_virtual_mode', true, false);
313
314 $candidates = [];
315 $globbed = glob(ABSPATH . 'sitemap*.xml');
316 if (is_array($globbed)) {
317 foreach ($globbed as $file) {
318 $basename = basename($file);
319 if ($basename === 'sitemap_index.xml' || preg_match('/^sitemap\d*\.xml$/', $basename)) {
320 $candidates[] = $file;
321 }
322 }
323 }
324 foreach (['news-sitemap.xml', 'video-sitemap.xml'] as $extra) {
325 $path = ABSPATH . $extra;
326 if (file_exists($path)) {
327 $candidates[] = $path;
328 }
329 }
330
331 if (empty($candidates)) {
332 return;
333 }
334
335 $virtual_index = get_option('metasync_sitemap_virtual_index', []);
336 $migrated_files = [];
337
338 foreach ($candidates as $file) {
339 $bn = basename($file);
340 $content = @file_get_contents($file);
341 if (false !== $content) {
342 $tkey = 'metasync_vsm_' . md5($bn);
343 set_transient($tkey, $content, 30 * DAY_IN_SECONDS);
344 if (false !== get_transient($tkey)) {
345 @unlink($file);
346 $virtual_index[$bn] = $tkey;
347 if ($bn !== 'sitemap_index.xml' && $bn !== 'news-sitemap.xml' && $bn !== 'video-sitemap.xml') {
348 $migrated_files[] = [
349 'filename' => $bn,
350 'url' => home_url('/' . $bn),
351 'lastmod' => current_time('mysql', true),
352 ];
353 }
354 }
355 }
356 }
357
358 update_option('metasync_sitemap_virtual_index', $virtual_index, false);
359
360 if (!empty($migrated_files)) {
361 update_option('metasync_sitemap_files', $migrated_files);
362 update_option('metasync_sitemap_last_generated', current_time('mysql'));
363 }
364 }
365
366 /**
367 * Check for plugin updates and run migrations if needed
368 */
369 function check_metasync_updates()
370 {
371 static $checked = false;
372 if ($checked) return;
373 $checked = true;
374
375 $current_version = get_option('metasync_version', '0.0.0');
376 $plugin_version = METASYNC_VERSION;
377
378 // If versions don't match, run migration
379 if (version_compare($current_version, $plugin_version, '<')) {
380 // Import whitelabel settings only if the JSON file is new or changed
381 // (prevents overwriting admin UI changes on every version check)
382 Metasync_Activator::check_whitelabel_settings_update();
383
384 // Run version-specific migrations first
385 MetaSync_DBMigration::run_version_migrations($current_version, $plugin_version);
386
387 // Migration for v2.7.0+: Remove AI Agent, switch to plugin auth token, make MCP always-on
388 if (version_compare($current_version, '2.7.0', '<')) {
389 // Remove old MCP API key option
390 delete_option('metasync_mcp_api_key');
391
392 // Remove MCP enabled/disabled toggle option (MCP is now always enabled)
393 delete_option('metasync_mcp_enabled');
394
395 // Remove AI Agent settings (AI Agent has been removed)
396 delete_option('metasync_ai_agent_mcp_config');
397 delete_option('metasync_ai_agent_ai_config');
398 delete_option('metasync_ai_agent_enabled');
399
400 // Ensure plugin auth token exists
401 $options = get_option('metasync_options', []);
402 if (empty($options['general']['apikey'])) {
403 $options['general']['apikey'] = wp_generate_password(32, false, false);
404 update_option('metasync_options', $options);
405 }
406 }
407
408 // WP-299: One-time purge of stale OTTO SEO cron backlog.
409 // Prior versions could accumulate thousands of metasync_process_seo_job and
410 // metasync_process_otto_crawl_url_job events due to unbounded rescheduling.
411 // Clear the backlog once on update; the new code prevents re-accumulation.
412 if (!get_option('metasync_wp299_cron_cleanup_done')) {
413 wp_unschedule_hook('metasync_process_seo_job');
414 wp_unschedule_hook('metasync_process_otto_crawl_url_job');
415 wp_unschedule_hook('metasync_process_otto_batch_cache_job');
416 update_option('metasync_wp299_cron_cleanup_done', true, false);
417 }
418
419 // WP-332: Migrate physical sitemap files on version update.
420 metasync_migrate_physical_sitemaps();
421
422 // Run full migration to ensure all tables are up to date
423 MetaSync_DBMigration::run_migrations();
424
425 // Update stored version
426 update_option('metasync_version', $plugin_version);
427
428 // Clear all cache plugins after update
429 Metasync_Cache_Purge::purge_all('plugin_update');
430
431 // Log the update
432 //error_log("MetaSync: Plugin updated from {$current_version} to {$plugin_version}. Database migration completed.");
433 }
434 }
435
436 // Hook into WordPress init to check for updates
437 add_action('init', 'check_metasync_updates', 1);
438
439 /**
440 * Handle whitelabel settings import after plugin is updated via WordPress admin
441 * This hook fires when plugins are installed/updated through the WordPress updater
442 *
443 * @param WP_Upgrader $upgrader WP_Upgrader instance
444 * @param array $hook_extra Extra arguments passed to hooked filters
445 */
446 function metasync_handle_plugin_upgrade($upgrader, $hook_extra)
447 {
448 // Only process plugin updates/installs
449 if (!isset($hook_extra['type']) || $hook_extra['type'] !== 'plugin') {
450 return;
451 }
452
453 // Only process install and update actions
454 if (!isset($hook_extra['action']) || !in_array($hook_extra['action'], ['install', 'update'], true)) {
455 return;
456 }
457
458 $this_plugin = plugin_basename(__FILE__);
459 $this_plugin_slug = dirname($this_plugin); // Get 'metasync' from 'metasync/metasync.php'
460 $should_import = false;
461
462 // Handle bulk updates
463 if (isset($hook_extra['bulk']) && $hook_extra['bulk'] === true && isset($hook_extra['plugins'])) {
464 foreach ($hook_extra['plugins'] as $plugin) {
465 // Match by exact path OR by plugin slug/directory
466 if ($plugin === $this_plugin || dirname($plugin) === $this_plugin_slug) {
467 $should_import = true;
468 break;
469 }
470 }
471 }
472
473 // Handle single plugin update/install
474 if (isset($hook_extra['plugin'])) {
475 $plugin = $hook_extra['plugin'];
476 // Match by exact path OR by plugin slug/directory
477 if ($plugin === $this_plugin || dirname($plugin) === $this_plugin_slug) {
478 $should_import = true;
479 }
480 }
481
482 // SPECIAL CASE: When uploading plugin via "Add New > Upload Plugin",
483 // WordPress doesn't set the 'plugin' parameter during 'install' action.
484 // Check if we can get the plugin info from the upgrader result or whitelabel file exists.
485 if (!$should_import && $hook_extra['action'] === 'install') {
486 // Check upgrader result for destination
487 if (isset($upgrader->result) && isset($upgrader->result['destination'])) {
488 $destination = $upgrader->result['destination'];
489 // Check if destination contains our plugin slug
490 if (strpos($destination, $this_plugin_slug) !== false) {
491 $should_import = true;
492 }
493 }
494
495 // Fallback: Check if whitelabel file exists in our plugin directory
496 // This means our plugin was just installed/updated with whitelabel settings
497 if (!$should_import) {
498 $whitelabel_file = Metasync_Activator::get_whitelabel_settings_file();
499 if ($whitelabel_file !== false) {
500 $should_import = true;
501 }
502 }
503 }
504
505 if ($should_import) {
506 Metasync_Activator::check_whitelabel_settings_update();
507 }
508 }
509
510 // Hook into WordPress upgrader to detect plugin updates
511 add_action('upgrader_process_complete', 'metasync_handle_plugin_upgrade', 10, 2);
512
513 /**
514 * Fallback: Check for whitelabel file changes on admin pages
515 * This handles edge cases where upgrader_process_complete doesn't fire
516 * (e.g., FTP uploads, manual file replacements)
517 * Only checks once per admin session to minimize performance impact
518 */
519 // function metasync_check_whitelabel_on_admin()
520 // {
521 // // Only check once per admin session to avoid overhead
522 // static $checked = false;
523 // if ($checked) {
524 // return;
525 // }
526 // $checked = true;
527
528 // require_once plugin_dir_path(__FILE__) . 'includes/class-metasync-activator.php';
529 // Metasync_Activator::check_whitelabel_settings_update();
530 // }
531
532 // Check for whitelabel changes on admin pages (fallback for edge cases)
533 // add_action('admin_init', 'metasync_check_whitelabel_on_admin', 1);
534
535 // Include Media Optimization Module
536 if (!metasync_is_non_metasync_admin_ajax()) {
537 require_once plugin_dir_path(__FILE__) . 'media-optimization/media-optimization-loader.php';
538 }
539
540 // Include Code Minification & Delivery Module
541 if (!metasync_is_non_metasync_admin_ajax()) {
542 require_once plugin_dir_path(__FILE__) . 'code-minification/code-minification-loader.php';
543 }
544
545
546 function run_metasync()
547 {
548 $plugin = new Metasync();
549 $plugin->run();
550 }
551 run_metasync();
552
553 /**
554 * Initialize WordPress MCP Server
555 *
556 * Safely register a single MCP tool class, logging failures without aborting
557 * subsequent registrations. Extracted as a named function so both production
558 * code and unit tests exercise the same logic (WP-265).
559 *
560 * @param object $server The MCP server instance (must have register_tool()).
561 * @param string $class_name Fully-qualified tool class name.
562 */
563 function metasync_safe_register_mcp_tool($server, $class_name) {
564 static $logged_missing = [];
565 if (!class_exists($class_name)) {
566 if (empty($logged_missing[$class_name])) {
567 error_log('MetaSync MCP: ' . $class_name . ' class not found — skipping registration. Run composer dump-autoload to regenerate the classmap.');
568 $logged_missing[$class_name] = true;
569 }
570 return;
571 }
572 try {
573 $server->register_tool(new $class_name());
574 } catch (\Throwable $e) {
575 error_log('MetaSync MCP: Failed to register tool ' . $class_name . ': ' . $e->getMessage());
576 }
577 }
578
579 /**
580 * Creates and configures the MCP server instance,
581 * registers all available MCP tools for WordPress operations.
582 * Hooked to 'init' for proper WordPress lifecycle integration.
583 */
584 function metasync_init_mcp_server() {
585 if (metasync_is_non_metasync_admin_ajax()) {
586 return;
587 }
588 // Skip the heavy MCP boot (server + sync logger + REST inventory + 100+ tool objects)
589 // unless this request is actually targeting the MCP REST route or was loaded via
590 // the stdio/HTTP bridge. WP-255.
591 if (!metasync_is_mcp_rest_request() && !defined('METASYNC_MCP_BRIDGE')) {
592 return;
593 }
594 // Initialize MCP server
595 global $metasync_mcp_server;
596
597 try {
598 $metasync_mcp_server = new Metasync_MCP_Server();
599
600 // Attach MCP sync logger so all write tool calls are recorded in Sync History.
601 new Metasync_MCP_Sync_Logger();
602
603 // SEO Inventory: shared builder + standalone REST endpoint (WP-135)
604 new Metasync_REST_SEO_Inventory();
605 } catch (\Throwable $e) {
606 error_log('MCP Server Initialization Error: ' . $e->getMessage());
607 return;
608 }
609
610 // Helper: register a single tool, logging failures without aborting subsequent registrations
611 $safe_register = function($class_name) use ($metasync_mcp_server) {
612 metasync_safe_register_mcp_tool($metasync_mcp_server, $class_name);
613 };
614
615 // Register MCP Tools (Total: 92 existing + 8 new = 100 tools total!)
616 // NEW in v2.8.0: +4 Taxonomy Meta tools, +4 Bulk Alt Text tools
617
618 // Post Meta Operations (4 tools)
619 $safe_register('MCP_Tool_Update_Post_Meta');
620 $safe_register('MCP_Tool_Get_Post_Meta');
621 $safe_register('MCP_Tool_Get_SEO_Meta');
622 $safe_register('MCP_Tool_Get_Hreflang_Links');
623
624 // Post Operations (4 tools)
625 $safe_register('MCP_Tool_Get_Post');
626 $safe_register('MCP_Tool_Get_Post_By_URL');
627 $safe_register('MCP_Tool_List_Posts');
628 $safe_register('MCP_Tool_Update_Post');
629 $safe_register('MCP_Tool_Get_Post_Types');
630
631 // SEO Analysis (3 tools)
632 $safe_register('MCP_Tool_Analyze_SEO');
633 $safe_register('MCP_Tool_Check_Indexability');
634 $safe_register('MCP_Tool_SEO_Health_Report');
635
636 // Search Operations (3 tools)
637 $safe_register('MCP_Tool_Search_Posts');
638 $safe_register('MCP_Tool_Search_By_Keyword');
639 $safe_register('MCP_Tool_Find_Missing_Meta');
640
641 // Redirect Management (5 tools)
642 $safe_register('MCP_Tool_Create_Redirect');
643 $safe_register('MCP_Tool_List_Redirects');
644 $safe_register('MCP_Tool_Delete_Redirect');
645 $safe_register('MCP_Tool_Update_Redirect');
646 $safe_register('MCP_Tool_Check_Redirects_Health');
647
648 // 404 Error Monitoring (5 tools)
649 $safe_register('MCP_Tool_List_404_Errors');
650 $safe_register('MCP_Tool_Get_404_Stats');
651 $safe_register('MCP_Tool_Delete_404_Error');
652 $safe_register('MCP_Tool_Clear_404_Errors');
653 $safe_register('MCP_Tool_Create_Redirect_From_404');
654
655 // Robots.txt & Sitemap Management (11 tools - 7 existing + 4 new)
656 $safe_register('MCP_Tool_Get_Robots_Txt');
657 $safe_register('MCP_Tool_Update_Robots_Txt');
658 $safe_register('MCP_Tool_Get_Sitemap_Status');
659 $safe_register('MCP_Tool_Regenerate_Sitemap');
660 $safe_register('MCP_Tool_Exclude_From_Sitemap');
661 $safe_register('MCP_Tool_Add_Robots_Rule');
662 $safe_register('MCP_Tool_Remove_Robots_Rule');
663 $safe_register('MCP_Tool_Parse_Robots_Txt');
664 $safe_register('MCP_Tool_Validate_Robots_Txt');
665 $safe_register('MCP_Tool_Get_News_Sitemap');
666 $safe_register('MCP_Tool_Get_Video_Sitemap');
667
668 // Plugin Settings Management (4 tools)
669 $safe_register('MCP_Tool_Get_Plugin_Settings');
670 $safe_register('MCP_Tool_Update_Plugin_Settings');
671 $safe_register('MCP_Tool_List_Plugin_Settings_Schema');
672 $safe_register('MCP_Tool_Get_MCP_Settings');
673
674 // Schema Markup Management (7 tools)
675 $safe_register('MCP_Tool_Get_Schema_Markup');
676 $safe_register('MCP_Tool_Update_Schema_Markup');
677 $safe_register('MCP_Tool_Add_Schema_Type');
678 $safe_register('MCP_Tool_Remove_Schema_Type');
679 $safe_register('MCP_Tool_Validate_Schema');
680 $safe_register('MCP_Tool_Get_Schema_Content');
681 $safe_register('MCP_Tool_Set_Schema_Content');
682
683 // Google Instant Index (6 tools)
684 $safe_register('MCP_Tool_Instant_Index_Update');
685 $safe_register('MCP_Tool_Instant_Index_Delete');
686 $safe_register('MCP_Tool_Instant_Index_Status');
687 $safe_register('MCP_Tool_Instant_Index_Bulk_Update');
688 $safe_register('MCP_Tool_Get_Instant_Index_Settings');
689 $safe_register('MCP_Tool_Update_Instant_Index_Settings');
690
691 // Custom HTML Pages (6 tools)
692 $safe_register('MCP_Tool_Create_Custom_Page');
693 $safe_register('MCP_Tool_Get_Custom_Page');
694 $safe_register('MCP_Tool_List_Custom_Pages');
695 $safe_register('MCP_Tool_Update_Custom_Page');
696 $safe_register('MCP_Tool_Delete_Custom_Page');
697 $safe_register('MCP_Tool_Import_LPS_Page');
698
699 // HTML to Builder Converter (3 tools)
700 $safe_register('MCP_Tool_Convert_HTML_To_Builder');
701 $safe_register('MCP_Tool_Create_Builder_Page_From_HTML');
702 $safe_register('MCP_Tool_Convert_Custom_Page_To_Builder');
703
704 // Code Snippets (6 tools)
705 $safe_register('MCP_Tool_Get_Header_Snippet');
706 $safe_register('MCP_Tool_Update_Header_Snippet');
707 $safe_register('MCP_Tool_Get_Footer_Snippet');
708 $safe_register('MCP_Tool_Update_Footer_Snippet');
709 $safe_register('MCP_Tool_Get_Post_Snippets');
710 $safe_register('MCP_Tool_Update_Post_Snippets');
711
712 // Categories & Taxonomies (15 tools)
713 $safe_register('MCP_Tool_List_Categories');
714 $safe_register('MCP_Tool_Get_Category');
715 $safe_register('MCP_Tool_Create_Category');
716 $safe_register('MCP_Tool_Update_Category');
717 $safe_register('MCP_Tool_Delete_Category');
718 $safe_register('MCP_Tool_Get_Post_Categories');
719 $safe_register('MCP_Tool_Set_Post_Categories');
720
721 // Tags (8 tools)
722 $safe_register('MCP_Tool_List_Tags');
723 $safe_register('MCP_Tool_Get_Tag');
724 $safe_register('MCP_Tool_Create_Tag');
725 $safe_register('MCP_Tool_Update_Tag');
726 $safe_register('MCP_Tool_Delete_Tag');
727 $safe_register('MCP_Tool_Get_Post_Tags');
728 $safe_register('MCP_Tool_Set_Post_Tags');
729
730 // Featured Images & Media (6 tools)
731 $safe_register('MCP_Tool_Get_Featured_Image');
732 $safe_register('MCP_Tool_Set_Featured_Image');
733 $safe_register('MCP_Tool_Upload_Featured_Image');
734 $safe_register('MCP_Tool_Remove_Featured_Image');
735 $safe_register('MCP_Tool_List_Media');
736 $safe_register('MCP_Tool_Get_Media_Details');
737
738 // Post CRUD Operations (1 tool - delete/restore disabled for safety)
739 $safe_register('MCP_Tool_Create_Post');
740 // $safe_register('MCP_Tool_Delete_Post'); // DISABLED - safety
741 // $safe_register('MCP_Tool_Restore_Post'); // DISABLED - safety
742
743 // Bulk Operations (3 tools - bulk delete disabled for safety)
744 $safe_register('MCP_Tool_Bulk_Update_Meta');
745 $safe_register('MCP_Tool_Bulk_Set_Categories');
746 $safe_register('MCP_Tool_Bulk_Change_Status');
747 // $safe_register('MCP_Tool_Bulk_Delete_Posts'); // DISABLED - safety
748
749 // WordPress Core SEO Settings (10 tools)
750 $safe_register('MCP_Tool_Get_Site_Info');
751 $safe_register('MCP_Tool_Update_Site_Info');
752 $safe_register('MCP_Tool_Get_Permalink_Structure');
753 $safe_register('MCP_Tool_Update_Permalink_Structure');
754 $safe_register('MCP_Tool_Get_Reading_Settings');
755 $safe_register('MCP_Tool_Update_Reading_Settings');
756 $safe_register('MCP_Tool_Get_Search_Visibility');
757 $safe_register('MCP_Tool_Update_Search_Visibility');
758 $safe_register('MCP_Tool_Get_Date_Format');
759 $safe_register('MCP_Tool_Get_Discussion_Settings');
760
761 // Taxonomy Meta Operations (4 tools - NEW in v2.8.0)
762 $safe_register('MCP_Tool_Get_Term_Meta');
763 $safe_register('MCP_Tool_Update_Term_Meta');
764 $safe_register('MCP_Tool_Bulk_Update_Term_Meta');
765 $safe_register('MCP_Tool_List_Terms_With_Meta');
766
767 // Bulk Alt Text Operations (4 tools - NEW in v2.8.0)
768 $safe_register('MCP_Tool_Audit_Alt_Text');
769 $safe_register('MCP_Tool_Bulk_Update_Alt_Text');
770 $safe_register('MCP_Tool_Generate_Alt_Text');
771 $safe_register('MCP_Tool_Validate_Alt_Text');
772
773 // OTTO Persistence Settings (2 tools)
774 $safe_register('MCP_Tool_Get_Otto_Persistence_Settings');
775 $safe_register('MCP_Tool_Update_Otto_Persistence_Settings');
776
777 // OTTO Pipeline Tools (3 tools)
778 $safe_register('MCP_Tool_Trigger_Otto_Optimization');
779 $safe_register('MCP_Tool_Get_Otto_Status');
780 $safe_register('MCP_Tool_Verify_SEO_Output');
781
782 // System Diagnostics & Plugin Info (4 tools)
783 $safe_register('MCP_Tool_System_Diagnostics');
784 $safe_register('MCP_Tool_List_All_Plugins');
785 $safe_register('MCP_Tool_Get_Cron_Jobs');
786 $safe_register('MCP_Tool_Get_WP_Option');
787
788 // SEO Inventory (1 tool — WP-135)
789 $safe_register('MCP_Tool_List_Posts_SEO_Inventory');
790
791 // Read-Only Database Access (3 tools)
792 $safe_register('MCP_Tool_DB_Tables');
793 $safe_register('MCP_Tool_DB_Describe');
794 $safe_register('MCP_Tool_DB_Select');
795
796 // Breadcrumb Tools (1 tool)
797 $safe_register('MCP_Tool_Get_Breadcrumb_Path');
798
799 // Cache Purge (2 tools)
800 $safe_register('MCP_Tool_Cache_Purge_All');
801 $safe_register('MCP_Tool_Cache_Purge_URL');
802
803 // LLMs.txt Tools (5 tools)
804 $safe_register('MCP_Tool_Get_LLMs_Txt');
805 $safe_register('MCP_Tool_Regenerate_LLMs_Txt');
806 $safe_register('MCP_Tool_Get_LLMs_Txt_Settings');
807 $safe_register('MCP_Tool_Update_LLMs_Txt_Settings');
808 $safe_register('MCP_Tool_Get_Post_Markdown');
809
810 // SEO Plugin Audit (4 tools — WP-202)
811 $safe_register('MCP_Tool_Read_SEO_Plugin_Data');
812 $safe_register('MCP_Tool_SEO_Plugin_Diff');
813 $safe_register('MCP_Tool_Sync_To_Active_Plugins');
814 $safe_register('MCP_Tool_Detect_SEO_Conflicts');
815
816 // Allow other plugins/themes to register tools
817 do_action('metasync_mcp_register_tools', $metasync_mcp_server);
818 }
819 add_action('init', 'metasync_init_mcp_server', 5);
820
821 /**
822 * Schedule a daily cron event to auto-purge Sync History records older than 90 days.
823 */
824 function metasync_schedule_sync_log_cleanup() {
825 if (!wp_next_scheduled('metasync_sync_log_daily_cleanup')) {
826 wp_schedule_event(time(), 'daily', 'metasync_sync_log_daily_cleanup');
827 }
828 }
829 add_action('wp', 'metasync_schedule_sync_log_cleanup');
830
831 /**
832 * Cron callback: delete Sync History records older than 90 days.
833 */
834 function metasync_sync_log_cleanup_handler() {
835 $sync_db = new Metasync_Sync_History_Database();
836 $sync_db->delete_older_than_days(90);
837 }
838 add_action('metasync_sync_log_daily_cleanup', 'metasync_sync_log_cleanup_handler');
839
840 /**
841 * Output DYO initialization flag to the frontend
842 * Makes window.__SA_DYO_INITIALIZED__ = true available in the DOM
843 * This indicates the Search Atlas plugin is active and initialized
844 */
845 function metasync_output_dyo_init_flag() {
846 echo '<script>window.__SA_DYO_INITIALIZED__=true;</script>' . "\n";
847 }
848 add_action('wp_head', 'metasync_output_dyo_init_flag', 1);
849
850 /**
851 * Initialize GA4 Analytics for Admin Area
852 * Hooked to admin_init for proper WordPress lifecycle integration
853 * Only loads after WordPress, plugins, and themes are fully loaded
854 */
855 function metasync_init_analytics() {
856 // Only initialize in admin area (excludes AJAX and REST API requests)
857 if (is_admin() && !wp_doing_ajax() && !defined('REST_REQUEST')) {
858 Metasync_GA4::get_instance();
859 }
860 }
861 add_action('admin_init', 'metasync_init_analytics', 10);
862
863 /**
864 * Initialize API Backoff System
865 * Hooked to init for proper WordPress lifecycle integration
866 * Monitors API responses and manages exponential backoff for rate limiting
867 * @since 2.7.1
868 */
869 function metasync_init_api_backoff() {
870 if (metasync_is_non_metasync_admin_ajax()) {
871 return;
872 }
873 // Initialize backoff manager (registers HTTP response filters)
874 Metasync_API_Backoff_Manager::get_instance();
875
876 // Initialize admin notices (only in admin area)
877 if (is_admin()) {
878 Metasync_API_Backoff_Notices::get_instance();
879 }
880 }
881 add_action('init', 'metasync_init_api_backoff', 5);
882
883 /**
884 * Initialize Review Notice
885 * Shows a dismissible notice asking users to rate the plugin after a usage period
886 * @since 2.8.0
887 */
888 function metasync_init_review_notice() {
889 if (metasync_is_non_metasync_admin_ajax()) {
890 return;
891 }
892 if (is_admin()) {
893 Metasync_Review_Notice::get_instance();
894 }
895 }
896 add_action('init', 'metasync_init_review_notice');
897
898 /**
899 * Global convenience function to get active JWT token
900 * Can be called from anywhere in WordPress (themes, other plugins, etc.)
901 *
902 * @param bool $force_refresh Force generation of new token even if cached one exists
903 * @return string|false JWT token on success, false on failure
904 */
905 if (!function_exists('metasync_get_jwt_token')) {
906 function metasync_get_jwt_token($force_refresh = false)
907 {
908 return Metasync::get_jwt_token($force_refresh);
909 }
910 }
911
912
913 /**
914 * Initialize Debug Mode Manager
915 * Hooked to 'init' for proper WordPress lifecycle integration
916 */
917 function metasync_init_debug_mode_manager() {
918 if (metasync_is_non_metasync_admin_ajax()) {
919 return;
920 }
921 // Initialize the Debug Mode Manager singleton
922 Metasync_Debug_Mode_Manager::get_instance();
923 }
924 add_action('init', 'metasync_init_debug_mode_manager', 10);
925
926 /**
927 * Oxygen Builder Compatibility
928 * Auto re-signs [oxygen] dynamic-data shortcodes when their HMAC signatures
929 * are invalid (e.g. after design-set import or site migration).
930 * Runs once on admin_init; skips entirely when Oxygen is inactive.
931 */
932 if (is_admin()) {
933 add_action('admin_init', ['Metasync_Oxygen_Compat', 'maybe_resign_shortcodes'], 20);
934 }
935