PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.14
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.14
2.6.26 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 All 138 releases
metasync / metasync.php

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

950 lines 33.5 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.14
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.14';
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 // WP-427: By the time upgrader_process_complete fires, the upgrader may have
454 // deleted the directory this (old, still-in-memory) copy of the plugin was
455 // loaded from — e.g. when the installed dir name differs from the package's
456 // root dir ('metasync-develop' vs 'metasync'). The Composer classmap then
457 // points at files that no longer exist, so autoloading Metasync_Activator
458 // below would fatal. Bail instead; the whitelabel re-import runs on the next
459 // request via check_metasync_updates() once the new copy is active.
460 if (!class_exists('Metasync_Activator', false)) {
461 $activator = __DIR__ . '/includes/class-metasync-activator.php';
462 if (!is_file($activator)) {
463 return;
464 }
465 require_once $activator;
466 }
467
468 // Only process install and update actions
469 if (!isset($hook_extra['action']) || !in_array($hook_extra['action'], ['install', 'update'], true)) {
470 return;
471 }
472
473 $this_plugin = plugin_basename(__FILE__);
474 $this_plugin_slug = dirname($this_plugin); // Get 'metasync' from 'metasync/metasync.php'
475 $should_import = false;
476
477 // Handle bulk updates
478 if (isset($hook_extra['bulk']) && $hook_extra['bulk'] === true && isset($hook_extra['plugins'])) {
479 foreach ($hook_extra['plugins'] as $plugin) {
480 // Match by exact path OR by plugin slug/directory
481 if ($plugin === $this_plugin || dirname($plugin) === $this_plugin_slug) {
482 $should_import = true;
483 break;
484 }
485 }
486 }
487
488 // Handle single plugin update/install
489 if (isset($hook_extra['plugin'])) {
490 $plugin = $hook_extra['plugin'];
491 // Match by exact path OR by plugin slug/directory
492 if ($plugin === $this_plugin || dirname($plugin) === $this_plugin_slug) {
493 $should_import = true;
494 }
495 }
496
497 // SPECIAL CASE: When uploading plugin via "Add New > Upload Plugin",
498 // WordPress doesn't set the 'plugin' parameter during 'install' action.
499 // Check if we can get the plugin info from the upgrader result or whitelabel file exists.
500 if (!$should_import && $hook_extra['action'] === 'install') {
501 // Check upgrader result for destination
502 if (isset($upgrader->result) && isset($upgrader->result['destination'])) {
503 $destination = $upgrader->result['destination'];
504 // Check if destination contains our plugin slug
505 if (strpos($destination, $this_plugin_slug) !== false) {
506 $should_import = true;
507 }
508 }
509
510 // Fallback: Check if whitelabel file exists in our plugin directory
511 // This means our plugin was just installed/updated with whitelabel settings
512 if (!$should_import) {
513 $whitelabel_file = Metasync_Activator::get_whitelabel_settings_file();
514 if ($whitelabel_file !== false) {
515 $should_import = true;
516 }
517 }
518 }
519
520 if ($should_import) {
521 Metasync_Activator::check_whitelabel_settings_update();
522 }
523 }
524
525 // Hook into WordPress upgrader to detect plugin updates
526 add_action('upgrader_process_complete', 'metasync_handle_plugin_upgrade', 10, 2);
527
528 /**
529 * Fallback: Check for whitelabel file changes on admin pages
530 * This handles edge cases where upgrader_process_complete doesn't fire
531 * (e.g., FTP uploads, manual file replacements)
532 * Only checks once per admin session to minimize performance impact
533 */
534 // function metasync_check_whitelabel_on_admin()
535 // {
536 // // Only check once per admin session to avoid overhead
537 // static $checked = false;
538 // if ($checked) {
539 // return;
540 // }
541 // $checked = true;
542
543 // require_once plugin_dir_path(__FILE__) . 'includes/class-metasync-activator.php';
544 // Metasync_Activator::check_whitelabel_settings_update();
545 // }
546
547 // Check for whitelabel changes on admin pages (fallback for edge cases)
548 // add_action('admin_init', 'metasync_check_whitelabel_on_admin', 1);
549
550 // Include Media Optimization Module
551 if (!metasync_is_non_metasync_admin_ajax()) {
552 require_once plugin_dir_path(__FILE__) . 'media-optimization/media-optimization-loader.php';
553 }
554
555 // Include Code Minification & Delivery Module
556 if (!metasync_is_non_metasync_admin_ajax()) {
557 require_once plugin_dir_path(__FILE__) . 'code-minification/code-minification-loader.php';
558 }
559
560
561 function run_metasync()
562 {
563 $plugin = new Metasync();
564 $plugin->run();
565 }
566 run_metasync();
567
568 /**
569 * Initialize WordPress MCP Server
570 *
571 * Safely register a single MCP tool class, logging failures without aborting
572 * subsequent registrations. Extracted as a named function so both production
573 * code and unit tests exercise the same logic (WP-265).
574 *
575 * @param object $server The MCP server instance (must have register_tool()).
576 * @param string $class_name Fully-qualified tool class name.
577 */
578 function metasync_safe_register_mcp_tool($server, $class_name) {
579 static $logged_missing = [];
580 if (!class_exists($class_name)) {
581 if (empty($logged_missing[$class_name])) {
582 error_log('MetaSync MCP: ' . $class_name . ' class not found — skipping registration. Run composer dump-autoload to regenerate the classmap.');
583 $logged_missing[$class_name] = true;
584 }
585 return;
586 }
587 try {
588 $server->register_tool(new $class_name());
589 } catch (\Throwable $e) {
590 error_log('MetaSync MCP: Failed to register tool ' . $class_name . ': ' . $e->getMessage());
591 }
592 }
593
594 /**
595 * Creates and configures the MCP server instance,
596 * registers all available MCP tools for WordPress operations.
597 * Hooked to 'init' for proper WordPress lifecycle integration.
598 */
599 function metasync_init_mcp_server() {
600 if (metasync_is_non_metasync_admin_ajax()) {
601 return;
602 }
603 // Skip the heavy MCP boot (server + sync logger + REST inventory + 100+ tool objects)
604 // unless this request is actually targeting the MCP REST route or was loaded via
605 // the stdio/HTTP bridge. WP-255.
606 if (!metasync_is_mcp_rest_request() && !defined('METASYNC_MCP_BRIDGE')) {
607 return;
608 }
609 // Initialize MCP server
610 global $metasync_mcp_server;
611
612 try {
613 $metasync_mcp_server = new Metasync_MCP_Server();
614
615 // Attach MCP sync logger so all write tool calls are recorded in Sync History.
616 new Metasync_MCP_Sync_Logger();
617
618 // SEO Inventory: shared builder + standalone REST endpoint (WP-135)
619 new Metasync_REST_SEO_Inventory();
620 } catch (\Throwable $e) {
621 error_log('MCP Server Initialization Error: ' . $e->getMessage());
622 return;
623 }
624
625 // Helper: register a single tool, logging failures without aborting subsequent registrations
626 $safe_register = function($class_name) use ($metasync_mcp_server) {
627 metasync_safe_register_mcp_tool($metasync_mcp_server, $class_name);
628 };
629
630 // Register MCP Tools (Total: 92 existing + 8 new = 100 tools total!)
631 // NEW in v2.8.0: +4 Taxonomy Meta tools, +4 Bulk Alt Text tools
632
633 // Post Meta Operations (4 tools)
634 $safe_register('MCP_Tool_Update_Post_Meta');
635 $safe_register('MCP_Tool_Get_Post_Meta');
636 $safe_register('MCP_Tool_Get_SEO_Meta');
637 $safe_register('MCP_Tool_Get_Hreflang_Links');
638
639 // Post Operations (4 tools)
640 $safe_register('MCP_Tool_Get_Post');
641 $safe_register('MCP_Tool_Get_Post_By_URL');
642 $safe_register('MCP_Tool_List_Posts');
643 $safe_register('MCP_Tool_Update_Post');
644 $safe_register('MCP_Tool_Get_Post_Types');
645
646 // SEO Analysis (3 tools)
647 $safe_register('MCP_Tool_Analyze_SEO');
648 $safe_register('MCP_Tool_Check_Indexability');
649 $safe_register('MCP_Tool_SEO_Health_Report');
650
651 // Search Operations (3 tools)
652 $safe_register('MCP_Tool_Search_Posts');
653 $safe_register('MCP_Tool_Search_By_Keyword');
654 $safe_register('MCP_Tool_Find_Missing_Meta');
655
656 // Redirect Management (5 tools)
657 $safe_register('MCP_Tool_Create_Redirect');
658 $safe_register('MCP_Tool_List_Redirects');
659 $safe_register('MCP_Tool_Delete_Redirect');
660 $safe_register('MCP_Tool_Update_Redirect');
661 $safe_register('MCP_Tool_Check_Redirects_Health');
662
663 // 404 Error Monitoring (5 tools)
664 $safe_register('MCP_Tool_List_404_Errors');
665 $safe_register('MCP_Tool_Get_404_Stats');
666 $safe_register('MCP_Tool_Delete_404_Error');
667 $safe_register('MCP_Tool_Clear_404_Errors');
668 $safe_register('MCP_Tool_Create_Redirect_From_404');
669
670 // Robots.txt & Sitemap Management (11 tools - 7 existing + 4 new)
671 $safe_register('MCP_Tool_Get_Robots_Txt');
672 $safe_register('MCP_Tool_Update_Robots_Txt');
673 $safe_register('MCP_Tool_Get_Sitemap_Status');
674 $safe_register('MCP_Tool_Regenerate_Sitemap');
675 $safe_register('MCP_Tool_Exclude_From_Sitemap');
676 $safe_register('MCP_Tool_Add_Robots_Rule');
677 $safe_register('MCP_Tool_Remove_Robots_Rule');
678 $safe_register('MCP_Tool_Parse_Robots_Txt');
679 $safe_register('MCP_Tool_Validate_Robots_Txt');
680 $safe_register('MCP_Tool_Get_News_Sitemap');
681 $safe_register('MCP_Tool_Get_Video_Sitemap');
682
683 // Plugin Settings Management (4 tools)
684 $safe_register('MCP_Tool_Get_Plugin_Settings');
685 $safe_register('MCP_Tool_Update_Plugin_Settings');
686 $safe_register('MCP_Tool_List_Plugin_Settings_Schema');
687 $safe_register('MCP_Tool_Get_MCP_Settings');
688
689 // Schema Markup Management (7 tools)
690 $safe_register('MCP_Tool_Get_Schema_Markup');
691 $safe_register('MCP_Tool_Update_Schema_Markup');
692 $safe_register('MCP_Tool_Add_Schema_Type');
693 $safe_register('MCP_Tool_Remove_Schema_Type');
694 $safe_register('MCP_Tool_Validate_Schema');
695 $safe_register('MCP_Tool_Get_Schema_Content');
696 $safe_register('MCP_Tool_Set_Schema_Content');
697
698 // Google Instant Index (6 tools)
699 $safe_register('MCP_Tool_Instant_Index_Update');
700 $safe_register('MCP_Tool_Instant_Index_Delete');
701 $safe_register('MCP_Tool_Instant_Index_Status');
702 $safe_register('MCP_Tool_Instant_Index_Bulk_Update');
703 $safe_register('MCP_Tool_Get_Instant_Index_Settings');
704 $safe_register('MCP_Tool_Update_Instant_Index_Settings');
705
706 // Custom HTML Pages (6 tools)
707 $safe_register('MCP_Tool_Create_Custom_Page');
708 $safe_register('MCP_Tool_Get_Custom_Page');
709 $safe_register('MCP_Tool_List_Custom_Pages');
710 $safe_register('MCP_Tool_Update_Custom_Page');
711 $safe_register('MCP_Tool_Delete_Custom_Page');
712 $safe_register('MCP_Tool_Import_LPS_Page');
713
714 // HTML to Builder Converter (3 tools)
715 $safe_register('MCP_Tool_Convert_HTML_To_Builder');
716 $safe_register('MCP_Tool_Create_Builder_Page_From_HTML');
717 $safe_register('MCP_Tool_Convert_Custom_Page_To_Builder');
718
719 // Code Snippets (6 tools)
720 $safe_register('MCP_Tool_Get_Header_Snippet');
721 $safe_register('MCP_Tool_Update_Header_Snippet');
722 $safe_register('MCP_Tool_Get_Footer_Snippet');
723 $safe_register('MCP_Tool_Update_Footer_Snippet');
724 $safe_register('MCP_Tool_Get_Post_Snippets');
725 $safe_register('MCP_Tool_Update_Post_Snippets');
726
727 // Categories & Taxonomies (15 tools)
728 $safe_register('MCP_Tool_List_Categories');
729 $safe_register('MCP_Tool_Get_Category');
730 $safe_register('MCP_Tool_Create_Category');
731 $safe_register('MCP_Tool_Update_Category');
732 $safe_register('MCP_Tool_Delete_Category');
733 $safe_register('MCP_Tool_Get_Post_Categories');
734 $safe_register('MCP_Tool_Set_Post_Categories');
735
736 // Tags (8 tools)
737 $safe_register('MCP_Tool_List_Tags');
738 $safe_register('MCP_Tool_Get_Tag');
739 $safe_register('MCP_Tool_Create_Tag');
740 $safe_register('MCP_Tool_Update_Tag');
741 $safe_register('MCP_Tool_Delete_Tag');
742 $safe_register('MCP_Tool_Get_Post_Tags');
743 $safe_register('MCP_Tool_Set_Post_Tags');
744
745 // Featured Images & Media (6 tools)
746 $safe_register('MCP_Tool_Get_Featured_Image');
747 $safe_register('MCP_Tool_Set_Featured_Image');
748 $safe_register('MCP_Tool_Upload_Featured_Image');
749 $safe_register('MCP_Tool_Remove_Featured_Image');
750 $safe_register('MCP_Tool_List_Media');
751 $safe_register('MCP_Tool_Get_Media_Details');
752
753 // Post CRUD Operations (1 tool - delete/restore disabled for safety)
754 $safe_register('MCP_Tool_Create_Post');
755 // $safe_register('MCP_Tool_Delete_Post'); // DISABLED - safety
756 // $safe_register('MCP_Tool_Restore_Post'); // DISABLED - safety
757
758 // Bulk Operations (3 tools - bulk delete disabled for safety)
759 $safe_register('MCP_Tool_Bulk_Update_Meta');
760 $safe_register('MCP_Tool_Bulk_Set_Categories');
761 $safe_register('MCP_Tool_Bulk_Change_Status');
762 // $safe_register('MCP_Tool_Bulk_Delete_Posts'); // DISABLED - safety
763
764 // WordPress Core SEO Settings (10 tools)
765 $safe_register('MCP_Tool_Get_Site_Info');
766 $safe_register('MCP_Tool_Update_Site_Info');
767 $safe_register('MCP_Tool_Get_Permalink_Structure');
768 $safe_register('MCP_Tool_Update_Permalink_Structure');
769 $safe_register('MCP_Tool_Get_Reading_Settings');
770 $safe_register('MCP_Tool_Update_Reading_Settings');
771 $safe_register('MCP_Tool_Get_Search_Visibility');
772 $safe_register('MCP_Tool_Update_Search_Visibility');
773 $safe_register('MCP_Tool_Get_Date_Format');
774 $safe_register('MCP_Tool_Get_Discussion_Settings');
775
776 // Taxonomy Meta Operations (4 tools - NEW in v2.8.0)
777 $safe_register('MCP_Tool_Get_Term_Meta');
778 $safe_register('MCP_Tool_Update_Term_Meta');
779 $safe_register('MCP_Tool_Bulk_Update_Term_Meta');
780 $safe_register('MCP_Tool_List_Terms_With_Meta');
781
782 // Bulk Alt Text Operations (4 tools - NEW in v2.8.0)
783 $safe_register('MCP_Tool_Audit_Alt_Text');
784 $safe_register('MCP_Tool_Bulk_Update_Alt_Text');
785 $safe_register('MCP_Tool_Generate_Alt_Text');
786 $safe_register('MCP_Tool_Validate_Alt_Text');
787
788 // OTTO Persistence Settings (2 tools)
789 $safe_register('MCP_Tool_Get_Otto_Persistence_Settings');
790 $safe_register('MCP_Tool_Update_Otto_Persistence_Settings');
791
792 // OTTO Pipeline Tools (3 tools)
793 $safe_register('MCP_Tool_Trigger_Otto_Optimization');
794 $safe_register('MCP_Tool_Get_Otto_Status');
795 $safe_register('MCP_Tool_Verify_SEO_Output');
796
797 // System Diagnostics & Plugin Info (4 tools)
798 $safe_register('MCP_Tool_System_Diagnostics');
799 $safe_register('MCP_Tool_List_All_Plugins');
800 $safe_register('MCP_Tool_Get_Cron_Jobs');
801 $safe_register('MCP_Tool_Get_WP_Option');
802
803 // SEO Inventory (1 tool — WP-135)
804 $safe_register('MCP_Tool_List_Posts_SEO_Inventory');
805
806 // Read-Only Database Access (3 tools)
807 $safe_register('MCP_Tool_DB_Tables');
808 $safe_register('MCP_Tool_DB_Describe');
809 $safe_register('MCP_Tool_DB_Select');
810
811 // Breadcrumb Tools (1 tool)
812 $safe_register('MCP_Tool_Get_Breadcrumb_Path');
813
814 // Cache Purge (2 tools)
815 $safe_register('MCP_Tool_Cache_Purge_All');
816 $safe_register('MCP_Tool_Cache_Purge_URL');
817
818 // LLMs.txt Tools (5 tools)
819 $safe_register('MCP_Tool_Get_LLMs_Txt');
820 $safe_register('MCP_Tool_Regenerate_LLMs_Txt');
821 $safe_register('MCP_Tool_Get_LLMs_Txt_Settings');
822 $safe_register('MCP_Tool_Update_LLMs_Txt_Settings');
823 $safe_register('MCP_Tool_Get_Post_Markdown');
824
825 // SEO Plugin Audit (4 tools — WP-202)
826 $safe_register('MCP_Tool_Read_SEO_Plugin_Data');
827 $safe_register('MCP_Tool_SEO_Plugin_Diff');
828 $safe_register('MCP_Tool_Sync_To_Active_Plugins');
829 $safe_register('MCP_Tool_Detect_SEO_Conflicts');
830
831 // Allow other plugins/themes to register tools
832 do_action('metasync_mcp_register_tools', $metasync_mcp_server);
833 }
834 add_action('init', 'metasync_init_mcp_server', 5);
835
836 /**
837 * Schedule a daily cron event to auto-purge Sync History records older than 90 days.
838 */
839 function metasync_schedule_sync_log_cleanup() {
840 if (!wp_next_scheduled('metasync_sync_log_daily_cleanup')) {
841 wp_schedule_event(time(), 'daily', 'metasync_sync_log_daily_cleanup');
842 }
843 }
844 add_action('wp', 'metasync_schedule_sync_log_cleanup');
845
846 /**
847 * Cron callback: delete Sync History records older than 90 days.
848 */
849 function metasync_sync_log_cleanup_handler() {
850 $sync_db = new Metasync_Sync_History_Database();
851 $sync_db->delete_older_than_days(90);
852 }
853 add_action('metasync_sync_log_daily_cleanup', 'metasync_sync_log_cleanup_handler');
854
855 /**
856 * Output DYO initialization flag to the frontend
857 * Makes window.__SA_DYO_INITIALIZED__ = true available in the DOM
858 * This indicates the Search Atlas plugin is active and initialized
859 */
860 function metasync_output_dyo_init_flag() {
861 echo '<script>window.__SA_DYO_INITIALIZED__=true;</script>' . "\n";
862 }
863 add_action('wp_head', 'metasync_output_dyo_init_flag', 1);
864
865 /**
866 * Initialize GA4 Analytics for Admin Area
867 * Hooked to admin_init for proper WordPress lifecycle integration
868 * Only loads after WordPress, plugins, and themes are fully loaded
869 */
870 function metasync_init_analytics() {
871 // Only initialize in admin area (excludes AJAX and REST API requests)
872 if (is_admin() && !wp_doing_ajax() && !defined('REST_REQUEST')) {
873 Metasync_GA4::get_instance();
874 }
875 }
876 add_action('admin_init', 'metasync_init_analytics', 10);
877
878 /**
879 * Initialize API Backoff System
880 * Hooked to init for proper WordPress lifecycle integration
881 * Monitors API responses and manages exponential backoff for rate limiting
882 * @since 2.7.1
883 */
884 function metasync_init_api_backoff() {
885 if (metasync_is_non_metasync_admin_ajax()) {
886 return;
887 }
888 // Initialize backoff manager (registers HTTP response filters)
889 Metasync_API_Backoff_Manager::get_instance();
890
891 // Initialize admin notices (only in admin area)
892 if (is_admin()) {
893 Metasync_API_Backoff_Notices::get_instance();
894 }
895 }
896 add_action('init', 'metasync_init_api_backoff', 5);
897
898 /**
899 * Initialize Review Notice
900 * Shows a dismissible notice asking users to rate the plugin after a usage period
901 * @since 2.8.0
902 */
903 function metasync_init_review_notice() {
904 if (metasync_is_non_metasync_admin_ajax()) {
905 return;
906 }
907 if (is_admin()) {
908 Metasync_Review_Notice::get_instance();
909 }
910 }
911 add_action('init', 'metasync_init_review_notice');
912
913 /**
914 * Global convenience function to get active JWT token
915 * Can be called from anywhere in WordPress (themes, other plugins, etc.)
916 *
917 * @param bool $force_refresh Force generation of new token even if cached one exists
918 * @return string|false JWT token on success, false on failure
919 */
920 if (!function_exists('metasync_get_jwt_token')) {
921 function metasync_get_jwt_token($force_refresh = false)
922 {
923 return Metasync::get_jwt_token($force_refresh);
924 }
925 }
926
927
928 /**
929 * Initialize Debug Mode Manager
930 * Hooked to 'init' for proper WordPress lifecycle integration
931 */
932 function metasync_init_debug_mode_manager() {
933 if (metasync_is_non_metasync_admin_ajax()) {
934 return;
935 }
936 // Initialize the Debug Mode Manager singleton
937 Metasync_Debug_Mode_Manager::get_instance();
938 }
939 add_action('init', 'metasync_init_debug_mode_manager', 10);
940
941 /**
942 * Oxygen Builder Compatibility
943 * Auto re-signs [oxygen] dynamic-data shortcodes when their HMAC signatures
944 * are invalid (e.g. after design-set import or site migration).
945 * Runs once on admin_init; skips entirely when Oxygen is inactive.
946 */
947 if (is_admin()) {
948 add_action('admin_init', ['Metasync_Oxygen_Compat', 'maybe_resign_shortcodes'], 20);
949 }
950