PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / trunk
Search Atlas SEO – OTTO AI SEO Automation for WordPress vtrunk
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 / admin / class-metasync-admin.php

class-metasync-admin.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress trunk, at admin/class-metasync-admin.php

6,560 lines 274.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // If this file is called directly, abort.
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7
8 /**
9 * The admin-specific functionality of the plugin.
10 *
11 * @link https://searchatlas.com
12 * @since 1.0.0
13 *
14 * @package Metasync
15 * @subpackage Metasync/admin
16 */
17
18 /**
19 * The admin-specific functionality of the plugin.
20 *
21 * Defines the plugin name, version, and two examples hooks for how to
22 * enqueue the admin-specific stylesheet and JavaScript.
23 *
24 * @package Metasync
25 * @subpackage Metasync/admin
26 * @author Engineering Team <support@searchatlas.com>
27 */
28 class Metasync_Admin
29 {
30
31 // Section constants for settings
32 const SECTION_FEATURES = "features_settings";
33 const SECTION_METASYNC = "metasync_settings";
34 const SECTION_SEARCHENGINE = "searchengine_settings";
35 const SECTION_LOCALSEO = "local_seo";
36 const SECTION_CODESNIPPETS = "code_snippets";
37 const SECTION_SITE_SETTINGS = "site_settings";
38 const SECTION_COMMON_SETTINGS = "common_settings";
39 const SECTION_COMMON_META_SETTINGS = "common_meta_settings";
40 const SECTION_SOCIAL_META = "social_meta";
41 const SECTION_SEO_CONTROLS = "seo_controls";
42 const SECTION_SEO_CONTROLS_ADVANCED = "seo_controls_advanced";
43 const SECTION_SEO_CONTROLS_INSTANT_INDEX = "seo_controls_instant_index";
44 const SECTION_PLUGIN_VISIBILITY = "plugin_visibility_settings";
45 const SECTION_LLMS_TXT = "llms_txt_settings";
46
47 /**
48 * The ID of this plugin.
49 *
50 * @since 1.0.0
51 * @access private
52 * @var string $plugin_name The ID of this plugin.
53 */
54 private $plugin_name;
55
56 /**
57 * The version of this plugin.
58 *
59 * @since 1.0.0
60 * @access private
61 * @var string $version The current version of this plugin.
62 */
63 private $version;
64
65 /**
66 * Holds the values to be used in the fields callbacks
67 */
68 private $options;
69 public $menu_title = "Search Atlas"; // Default, overridden by get_effective_menu_title()
70 public const page_title = "MetaSync Settings";
71
72 /**
73 * Get effective menu title with whitelabel company name
74 */
75 public function get_effective_menu_title()
76 {
77 $whitelabel_company_name = Metasync::get_whitelabel_company_name();
78
79 if ($whitelabel_company_name) {
80 return $whitelabel_company_name . ' SEO';
81 }
82
83 // Use centralized method for getting effective plugin name
84 return Metasync::get_effective_plugin_name();
85 }
86 public const option_group = "metasync_group";
87 public const option_key = "metasync_options";
88 public static $page_slug = "searchatlas";
89 /**
90 * Get the effective dashboard domain
91 * Returns whitelabel domain if set, otherwise returns default production domain
92 * Uses centralized constant from main Metasync class
93 */
94 public static function get_effective_dashboard_domain()
95 {
96 // Delegate to main Metasync class for consistent domain resolution
97 return Metasync::get_dashboard_domain();
98 }
99
100 /**
101 * Static method to render header and navigation for external pages (like AI Agent)
102 * This creates a minimal instance just for rendering
103 */
104 public static function render_standard_header_nav($page_title = null, $current_page = null) {
105 Metasync_Admin_Navigation::instance()->render_standard_header_nav($page_title, $current_page);
106 }
107
108 /**
109 * Static header render
110 */
111 public static function render_static_header($page_title = null) {
112 Metasync_Admin_Navigation::instance()->render_static_header($page_title);
113 }
114
115 /**
116 * Static navigation render
117 */
118 public static function render_static_navigation($current_page = null) {
119 Metasync_Admin_Navigation::instance()->render_static_navigation($current_page);
120 }
121
122 /**
123 * Get dashboard URL with authentication tokens and tracking parameters
124 * Returns the complete URL for accessing the Search Atlas dashboard
125 */
126 public function get_dashboard_url()
127 {
128 // Get the effective dashboard domain (whitelabel or production)
129 $dashboard_url = self::get_effective_dashboard_domain();
130
131 // Get current options for token inclusion
132 $general_options = Metasync::get_option('general');
133
134 // Add JWT token if available for seamless login
135 if (isset($general_options['linkgraph_token']) && !empty($general_options['linkgraph_token'])) {
136 $dashboard_url .= '/?jwtToken=' . urlencode($general_options['linkgraph_token']);
137 }
138
139 // Add source tracking parameter
140 $dashboard_url .= (strpos($dashboard_url, '?') !== false ? '&' : '?') . 'source=wordpress-plugin';
141
142 // Add whitelabel identification if in whitelabel mode
143 $whitelabel_company_name = Metasync::get_whitelabel_company_name();
144 if ($whitelabel_company_name) {
145 $dashboard_url .= '&whitelabel=' . urlencode($whitelabel_company_name);
146 }
147
148 return $dashboard_url;
149 }
150
151 public const feature_sections = array(
152 'enable_404monitor' => 'Enable 404 Monitor',
153 'enable_siteverification' => 'Enable Site Verification',
154 'enable_localbusiness' => 'Enable Local Business',
155 'enable_codesnippets' => 'Enable Code Snippets',
156 'enable_googleconsole' => 'Enable Google Console',
157 'enable_optimalsettings' => 'Enable Optimal Settings',
158 'enable_globalsettings' => 'Enable Global Settings',
159 'enable_commonmetastatus' => 'Enable Common Meta Status',
160 'enable_socialmeta' => 'Enable Social Meta',
161 'enable_redirections' => 'Enable Redirections',
162 'enable_errorlogs' => 'Enable Error Logs'
163 );
164
165 private $database;
166 private $db_redirection;
167 public $db_heartbeat_errors;
168 public $setup_wizard;
169
170
171 /**
172 * Initialize the class and set its properties.
173 *
174 * @since 1.0.0
175 * @param string $plugin_name The name of this plugin.
176 * @param string $version The version of this plugin.
177 */
178
179 public function __construct($plugin_name, $version, &$database, $db_redirection, $db_heartbeat_errors) // , $data_error_log_list
180 {
181
182 $this->plugin_name = $plugin_name;
183 $this->version = $version;
184 $this->database = $database;
185 $this->db_redirection = $db_redirection;
186 $this->db_heartbeat_errors = $db_heartbeat_errors;
187 // $this->data_error_log_list = $data_error_log_list;
188
189 // Initialize setup wizard
190 $this->setup_wizard = new Metasync_Setup_Wizard($plugin_name, $version);
191
192 // Wire up the extracted settings-fields singleton
193 Metasync_Settings_Fields::instance()->set_admin_instance($this);
194
195 // Get data first for menu configuration
196 $data = Metasync::get_option('general');
197
198 // Set menu title using the effective title (includes whitelabel company name if available)
199 $this->menu_title = $this->get_effective_menu_title();
200 $raw_slug = isset($data['white_label_plugin_menu_slug']) ? $data['white_label_plugin_menu_slug'] : '';
201 $clean_slug = sanitize_title($raw_slug);
202 # Self-heal a legacy URL-shaped slug: persist the sanitized value so every
203 # reader that builds admin links from the stored option gets a valid WP menu slug.
204 if ($raw_slug !== '' && $clean_slug !== $raw_slug) {
205 $options = Metasync::get_option();
206 $options['general']['white_label_plugin_menu_slug'] = $clean_slug;
207 Metasync::set_option($options);
208 }
209 self::$page_slug = $clean_slug === '' ? 'searchatlas' : $clean_slug;
210
211 add_action('admin_menu', array($this, 'add_plugin_settings_page'));
212 add_action('admin_menu', array($this, 'add_import_external_data_page'));
213 add_action('admin_init', array($this, 'settings_page_init'));
214 add_filter('all_plugins', array($this,'metasync_plugin_white_label'));
215 add_filter( 'plugin_row_meta',array($this,'metasync_view_detials_url'),10,3);
216 add_filter('site_transient_update_plugins', array($this, 'inject_whitelabel_icon_into_update_transient'));
217
218 // Display transient error/success messages for redirections
219 add_action('admin_notices', array($this, 'display_redirection_messages'));
220
221 // Display CPU deferral notices when batch processing is deferred
222 add_action('admin_notices', array($this, 'display_cpu_deferral_notice'));
223
224 // Display LLMs.txt cross-plugin conflict notice
225 add_action('admin_notices', array($this, 'display_llms_txt_conflict_notice'));
226
227 // Add custom column for HTML-converted pages
228 add_filter('manage_posts_columns', array($this, 'add_html_converted_column'));
229 add_filter('manage_pages_columns', array($this, 'add_html_converted_column'));
230 add_action('manage_posts_custom_column', array($this, 'render_html_converted_column'), 10, 2);
231 add_action('manage_pages_custom_column', array($this, 'render_html_converted_column'), 10, 2);
232
233 // Add badge to page editor screen
234 add_action('edit_form_after_title', array($this, 'add_editor_source_notice'));
235
236 // Add badge to quick edit panel
237 add_action('quick_edit_custom_box', array($this, 'add_quick_edit_source_display'), 10, 2);
238
239 // Add dashboard widget
240 add_action('wp_dashboard_setup', array($this, 'add_html_pages_dashboard_widget'));
241
242 // Add Search Atlas status to WordPress admin bar (priority 999 to ensure plugin is fully loaded)
243 // Always add the action - the method will check the setting internally
244 add_action('admin_bar_menu', array($this, 'add_searchatlas_admin_bar_status'), 999);
245
246 #add css into admin header for icon image
247
248 add_action('admin_head', array($this,'metasync_admin_icon_style'));
249 add_action('admin_head', array($this, 'metasync_fouc_prevention_style'));
250 add_action('admin_head', array($this, 'suppress_notices_on_wizard_page'), 1);
251
252 // Always add admin bar styles - the method will check the setting internally
253 add_action('wp_head', array($this,'metasync_admin_bar_style')); // For frontend admin bar
254 add_action('admin_head', array($this,'metasync_admin_bar_style')); // For backend admin bar
255 // removing this as we don't need it anymore because we are using wp-ajax to implement the white label
256 // add_action('update_option_metasync_options', array($this, 'check_and_redirect_slug'), 10, 3);
257
258 // Sync plugin file headers whenever metasync_options is updated (covers AJAX save, Settings API, import)
259 add_action('update_option_metasync_options', array($this, 'on_options_updated_sync_file_headers'), 10, 2);
260
261 // Invalidate the admin bar status cache when settings change or the API key is rotated.
262 add_action('update_option_metasync_options', array('Metasync_Admin_Navigation', 'invalidate_admin_bar_status_cache'), 10, 0);
263 add_action('metasync_api_key_changed', array('Metasync_Admin_Navigation', 'invalidate_admin_bar_status_cache'), 10, 0);
264
265 add_action('admin_init', array($this, 'initialize_cookie'));
266 add_action('admin_init', array($this, 'maybe_redirect_to_wizard'));
267
268 // Add admin_post hooks for form submissions (WordPress standard way - no output buffering needed)
269 add_action('admin_post_metasync_clear_all_cache_plugins', array($this, 'handle_clear_all_cache_plugins'));
270 add_action('admin_post_metasync_clear_otto_cache_all', array($this, 'handle_clear_otto_cache_all'));
271 add_action('admin_post_metasync_clear_otto_cache_url', array($this, 'handle_clear_otto_cache_url'));
272 add_action('admin_post_metasync_purge_hosting_cache', array($this, 'handle_purge_hosting_cache'));
273
274 // Add AJAX for saving general settings
275 add_action( 'wp_ajax_meta_sync_save_settings', array($this,'meta_sync_save_settings') );
276
277 // Add AJAX for saving Indexation Control settings
278 add_action( 'wp_ajax_meta_sync_save_seo_controls', array($this,'meta_sync_save_seo_controls') );
279
280 // Add AJAX handler for saving Performance (CPU Load) settings
281 add_action('wp_ajax_metasync_save_performance_settings', array($this, 'ajax_save_performance_settings'));
282
283 // Add AJAX for saving execution settings
284 add_action( 'wp_ajax_metasync_save_execution_settings', array($this, 'ajax_save_execution_settings') );
285
286 // Add AJAX for saving hosting cache settings
287 add_action( 'wp_ajax_metasync_save_hosting_cache_settings', array($this, 'ajax_save_hosting_cache_settings') );
288
289 // Add AJAX for saving OTTO Cache TTL
290 add_action('wp_ajax_metasync_save_otto_cache_ttl', array($this, 'ajax_save_otto_cache_ttl'));
291 add_action( 'wp_ajax_metasync_save_object_cache_settings', array($this, 'ajax_save_object_cache_settings') );
292
293 // Add AJAX for saving edge cache / CDN settings
294 add_action( 'wp_ajax_metasync_save_edge_cache_settings', array('Metasync_Edge_Cache_Settings', 'ajax_save') );
295 // Add AJAX handler for Plugin Auth Token refresh
296 add_action('wp_ajax_metasync_refresh_plugin_auth_token', array($this, 'refresh_plugin_auth_token'));
297
298 // Add AJAX handler to get current Plugin Auth Token (for UI updates)
299 add_action('wp_ajax_metasync_get_plugin_auth_token', array($this, 'get_plugin_auth_token'));
300
301 // Add AJAX handler for creating redirects from 404 suggestions
302 add_action('wp_ajax_metasync_create_redirect_from_404', array($this, 'ajax_create_redirect_from_404'));
303
304 // Add AJAX handler for updating database structure
305 add_action('wp_ajax_metasync_update_db_structure', array($this, 'ajax_update_db_structure'));
306
307 // Add AJAX handlers for setup wizard
308 add_action('wp_ajax_metasync_save_wizard_progress', array($this, 'ajax_save_wizard_progress'));
309 add_action('wp_ajax_metasync_complete_wizard', array($this, 'ajax_complete_wizard'));
310
311 // Add AJAX handlers for robots.txt
312 add_action('wp_ajax_metasync_validate_robots', array($this, 'ajax_validate_robots'));
313 add_action('wp_ajax_metasync_get_default_robots', array($this, 'ajax_get_default_robots'));
314 add_action('wp_ajax_metasync_preview_robots_backup', array($this, 'ajax_preview_robots_backup'));
315 add_action('wp_ajax_metasync_delete_robots_backup', array($this, 'ajax_delete_robots_backup'));
316 add_action('wp_ajax_metasync_restore_robots_backup', array($this, 'ajax_restore_robots_backup'));
317 add_action('wp_ajax_metasync_get_robots_backups', array($this, 'ajax_get_robots_backups'));
318
319 // Add AJAX handlers for host blocking test
320 add_action('wp_ajax_metasync_test_host_blocking_get', array($this, 'ajax_test_host_blocking_get'));
321 add_action('wp_ajax_metasync_test_host_blocking_post', array($this, 'ajax_test_host_blocking_post'));
322
323 // Add AJAX handlers for OTTO excluded URLs
324 add_action('wp_ajax_metasync_otto_add_excluded_url', array($this, 'ajax_otto_add_excluded_url'));
325 add_action('wp_ajax_metasync_otto_delete_excluded_url', array($this, 'ajax_otto_delete_excluded_url'));
326 add_action('wp_ajax_metasync_burst_ping', array($this, 'ajax_burst_ping'));
327 add_action('wp_ajax_metasync_otto_get_excluded_urls', array($this, 'ajax_otto_get_excluded_urls'));
328 add_action('wp_ajax_metasync_otto_recheck_excluded_url', array($this, 'ajax_otto_recheck_excluded_url'));
329
330 # Add AJAX handler for GA4 analytics tracking
331 add_action('wp_ajax_metasync_track_one_click_activation', array($this, 'ajax_track_one_click_activation'));
332
333 # Add AJAX handler for submitting issue reports
334 add_action('wp_ajax_metasync_submit_issue_report', array($this, 'ajax_submit_issue_report'));
335
336 # Add AJAX handlers for support token management
337
338 # Add AJAX handler for theme switcher
339 add_action('wp_ajax_metasync_save_theme', array($this, 'ajax_save_theme'));
340
341 # Add AJAX handlers for Sync Log management
342 add_action('wp_ajax_metasync_clear_sync_log', array($this, 'ajax_clear_sync_log'));
343 add_action('wp_ajax_metasync_rollback_mcp_change', array($this, 'ajax_rollback_mcp_change'));
344
345 # Add AJAX handler for external data import
346 add_action('wp_ajax_metasync_import_external_data', array($this, 'ajax_import_external_data'));
347
348 # Add AJAX handler for SEO metadata batch import
349 add_action('wp_ajax_metasync_import_seo_metadata', array($this, 'ajax_import_seo_metadata'));
350
351 # Add AJAX handler for password recovery
352 add_action('wp_ajax_metasync_recover_password', array($this, 'ajax_recover_password'));
353
354 # Add AJAX handler for resetting bot statistics
355 add_action('wp_ajax_metasync_reset_bot_stats', array($this, 'ajax_reset_bot_stats'));
356
357 # Add AJAX handlers for DB cleanup
358 add_action('wp_ajax_metasync_run_db_cleanup', array($this, 'ajax_run_db_cleanup'));
359 add_action('wp_ajax_metasync_save_db_cleanup_settings', array($this, 'ajax_save_db_cleanup_settings'));
360
361 # Add admin-post handler for exporting whitelabel settings (file download)
362 add_action('admin_post_metasync_export_whitelabel_settings', array($this, 'handle_export_whitelabel_settings'));
363
364 # Media Optimization AJAX handlers
365 add_action('wp_ajax_metasync_optimize_single_image', array($this, 'ajax_optimize_single_image'));
366 add_action('wp_ajax_metasync_revert_single_image', array($this, 'ajax_revert_single_image'));
367 add_action('wp_ajax_metasync_start_batch_optimize', array($this, 'ajax_start_batch_optimize'));
368 add_action('wp_ajax_metasync_cancel_batch_optimize', array($this, 'ajax_cancel_batch_optimize'));
369 add_action('wp_ajax_metasync_batch_progress', array($this, 'ajax_batch_progress'));
370 add_action('wp_ajax_metasync_bulk_optimize_selected', array($this, 'ajax_bulk_optimize_selected'));
371 add_action('wp_ajax_metasync_bulk_unoptimize_selected', array($this, 'ajax_bulk_unoptimize_selected'));
372 add_action('wp_ajax_metasync_process_batch_tick', array($this, 'ajax_process_batch_tick'));
373 add_action('wp_ajax_metasync_delete_orphaned_image', array($this, 'ajax_delete_orphaned_image'));
374 add_action('metasync_media_batch_optimize_cron', array($this, 'handle_media_batch_cron'));
375
376 # Add AJAX handlers for Google Instant Indexing
377 add_action('wp_ajax_metasync_send_giapi', array($this, 'ajax_send_giapi'));
378
379 # Add AJAX handlers for Bing Instant Indexing (IndexNow)
380 add_action('wp_ajax_metasync_send_bing_indexnow', array($this, 'ajax_send_bing_indexnow'));
381
382 # Add hooks for instant indexing settings saves
383 add_action('admin_init', array($this, 'save_instant_indexing_settings'));
384
385 # Add hooks for instant indexing post actions
386 add_filter('post_row_actions', array($this, 'add_instant_indexing_post_actions'), 10, 2);
387 add_filter('page_row_actions', array($this, 'add_instant_indexing_post_actions'), 10, 2);
388
389 # Add hooks for auto-submission on post publish
390 add_action('save_post', array($this, 'auto_submit_to_instant_indexing'), 10, 3);
391
392 // Add REST API endpoint for ping
393 add_action('rest_api_init', array($this, 'register_ping_rest_endpoint'));
394
395 // Add heartbeat cron functionality
396 add_filter('cron_schedules', array($this, 'add_heartbeat_cron_schedule'));
397 add_action('metasync_heartbeat_cron_check', array($this, 'execute_heartbeat_cron_check'));
398 add_action('metasync_burst_heartbeat', array($this, 'execute_burst_heartbeat'));
399 add_action('metasync_announce_cron', array($this, 'execute_announce_cron'));
400
401 // PR3: Transition to KEY_PENDING when API key is added or rotated
402 add_action('metasync_heartbeat_state_key_pending', array($this, 'set_heartbeat_state_key_pending'));
403
404 // Add transient cleanup cron functionality
405 add_action('metasync_cleanup_transients', array($this, 'execute_transient_cleanup'));
406
407 // Add DB cleanup cron functionality
408 add_action('metasync_db_cleanup', array($this, 'execute_db_cleanup'));
409
410 // Schedule heartbeat cron on plugin load (if not already scheduled)
411 add_action('init', array($this, 'maybe_schedule_heartbeat_cron'));
412
413 // Schedule transient cleanup cron on plugin load
414 add_action('init', array($this, 'maybe_schedule_transient_cleanup_cron'));
415
416 // Schedule DB cleanup cron on plugin load (only when enabled)
417 add_action('init', array($this, 'maybe_schedule_db_cleanup_cron'));
418
419 # Schedule hidden post manager cron on plugin load (runs every 7 days)
420 add_action('init', array($this, 'maybe_schedule_hidden_post_check'));
421
422 # Schedule OTTO 404 exclusion recheck (runs daily to recheck URLs excluded 7+ days ago)
423 add_action('init', array($this, 'maybe_schedule_otto_recheck_404_cron'));
424
425 # Schedule support token cleanup cron on plugin load (runs daily)
426
427 // Listen for immediate heartbeat trigger requests from other parts of the plugin
428 add_action('metasync_trigger_immediate_heartbeat', array($this, 'handle_immediate_heartbeat_trigger'));
429
430 // Listen for cron scheduling requests (after Search Atlas connect authentication)
431 add_action('metasync_ensure_heartbeat_cron_scheduled', array($this, 'maybe_schedule_heartbeat_cron'));
432
433 // Action Scheduler configuration filters
434 add_filter('action_scheduler_queue_runner_concurrent_batches', array($this, 'get_action_scheduler_batches'));
435 add_filter('action_scheduler_retention_period', array($this, 'get_action_scheduler_retention_period'));
436
437 // Note: Option change monitoring is now handled by the centralized API Key Monitor class
438 // This provides more comprehensive and intelligent monitoring of API key changes
439
440
441 #--------------------------
442 # we are disabling this code
443 # To prevent enabling debuging on every pluging Update
444 # This causes Issue #102 on gilab
445 #--------------------------
446 #
447 # NOTE:
448 # Do not delete this as we may need it in implementing universal logging
449
450 # add_action('upgrader_process_complete', array($this,'metasync_plugin_updated_action'), 10, 2);
451
452 # Hook into post category creation and update (AFTER they are saved)
453 add_action('saved_term', array($this,'admin_crud_term'), 10, 3);
454
455 # Hook into post category deletion (AFTER it is deleted)
456 // add_action('pre_delete_term', array($this,'admin_delete_term'), 10, 2);
457 # Using delete_category hook which fires AFTER category is fully deleted and provides correct data
458 add_action('delete_category', array($this,'admin_delete_category'), 10, 1);
459
460 }
461 /*
462 This function add css to wp admin header
463 */
464 public function metasync_admin_icon_style(){
465 # Get Metasync Option
466
467 $data= Metasync::get_option('general');
468
469 # Get white label menu slug
470 # Sanitize so a URL-shaped legacy value still yields a valid WP menu slug
471 $menu_slug = empty($data['white_label_plugin_menu_slug']) ? self::$page_slug : sanitize_title($data['white_label_plugin_menu_slug']);
472 $menu_slug = $menu_slug === '' ? self::$page_slug : $menu_slug;
473
474 ?>
475 <style>
476 #toplevel_page_<?php echo esc_attr(str_replace(' ', '-',$menu_slug)); ?> .wp-menu-image.dashicons-before img {
477 width: 36px;
478 height: 34px;
479 padding: 0!important;
480 object-fit: contain;
481 object-position: center;
482 }
483 #toplevel_page_<?php echo esc_attr(str_replace(' ', '-',$menu_slug)); ?> .wp-menu-image img {
484 width: 20px !important;
485 }
486 </style>
487 <?php
488 }
489
490 public function metasync_fouc_prevention_style() {
491 if ( ! isset( $_GET['page'] ) || strpos( $_GET['page'], self::$page_slug ) !== 0 ) {
492 return;
493 }
494 ?>
495 <style>
496 .metasync-dashboard-wrap { opacity: 0; transition: opacity 0.15s ease-in; }
497 /*
498 * WP prints admin notices at the top of #wpbody-content, then core's
499 * common.js relocates them next to .wp-header-end inside our wrap on
500 * DOM-ready. Keep them out of the flow until that move so they don't
501 * flash above the dashboard header and shove the page down.
502 * Once relocated they're no longer a direct child here, so this rule
503 * stops matching and they fade in with the wrap. We mirror core's
504 * relocation set exactly (.notice/.updated/.error, minus .inline and
505 * .below-h2); .update-nag is left alone since core never moves it.
506 */
507 #wpbody-content > .notice:not(.inline):not(.below-h2),
508 #wpbody-content > .updated:not(.inline):not(.below-h2),
509 #wpbody-content > .error:not(.inline):not(.below-h2) { display: none !important; }
510 </style>
511 <?php
512 }
513
514 public function suppress_notices_on_wizard_page() {
515 if ( ! isset( $_GET['page'] ) ) {
516 return;
517 }
518
519 $page = sanitize_text_field( wp_unslash( $_GET['page'] ) );
520
521 // Setup wizard: strip every notice for a fully focused, distraction-free screen.
522 if ( strpos( $page, '-setup-wizard' ) !== false ) {
523 remove_all_actions( 'admin_notices' );
524 remove_all_actions( 'all_admin_notices' );
525 return;
526 }
527
528 // Other Search Atlas plugin pages: strip third-party notices but keep our own.
529 if ( strpos( $page, self::$page_slug ) === 0 ) {
530 $this->remove_third_party_admin_notices();
531 }
532 }
533
534 /**
535 * Remove admin-notice callbacks that don't belong to this plugin.
536 *
537 * Third-party plugins (e.g. Rank Math, Yoast) register their banners on the
538 * global admin_notices / all_admin_notices actions, which WordPress fires on
539 * every admin screen — so their notices leak onto our settings pages. We walk
540 * the registered callbacks and drop any whose code lives outside this plugin's
541 * directory, leaving every one of our own notices intact.
542 */
543 private function remove_third_party_admin_notices() {
544 global $wp_filter;
545
546 foreach ( array( 'admin_notices', 'all_admin_notices' ) as $hook ) {
547 if ( empty( $wp_filter[ $hook ] ) || ! ( $wp_filter[ $hook ] instanceof WP_Hook ) ) {
548 continue;
549 }
550
551 // Collect first, then remove — avoids mutating the callbacks array mid-walk.
552 $to_remove = array();
553 foreach ( $wp_filter[ $hook ]->callbacks as $priority => $callbacks ) {
554 foreach ( $callbacks as $callback ) {
555 if ( ! $this->is_own_admin_notice_callback( $callback['function'] ) ) {
556 $to_remove[] = array( $callback['function'], $priority );
557 }
558 }
559 }
560
561 foreach ( $to_remove as $entry ) {
562 remove_action( $hook, $entry[0], $entry[1] );
563 }
564 }
565 }
566
567 /**
568 * Determine whether an admin-notice callback is defined by this plugin.
569 *
570 * Ownership is decided by where the callback's code physically lives, not by
571 * class name — our notice owners use inconsistent prefixes (Metasync_Admin,
572 * Google_Index_Admin, ConfigControllerMetaSync), and a white-label install can
573 * rename the plugin folder. Resolving the declaring file and checking it sits
574 * inside this plugin's directory is robust against all of that. If a callback
575 * can't be introspected, we keep it rather than risk hiding a legitimate notice.
576 *
577 * @param string|array|Closure $function The registered callback.
578 * @return bool True when the callback's code lives inside this plugin.
579 */
580 private function is_own_admin_notice_callback( $function ) {
581 // Plugin root: this file lives in <plugin>/admin, so its parent is the root.
582 $plugin_dir = wp_normalize_path( dirname( __DIR__ ) ) . '/';
583
584 try {
585 if ( is_array( $function ) && isset( $function[0], $function[1] ) ) {
586 $class = is_object( $function[0] ) ? get_class( $function[0] ) : $function[0];
587 $ref = new ReflectionMethod( $class, $function[1] );
588 } elseif ( $function instanceof Closure ) {
589 $ref = new ReflectionFunction( $function );
590 } elseif ( is_string( $function ) && strpos( $function, '::' ) !== false ) {
591 list( $class, $method ) = explode( '::', $function, 2 );
592 $ref = new ReflectionMethod( $class, $method );
593 } elseif ( is_string( $function ) ) {
594 $ref = new ReflectionFunction( $function );
595 } else {
596 return false;
597 }
598 } catch ( \Throwable $e ) {
599 // Unintrospectable callback — keep it; never hide a notice we can't classify.
600 return true;
601 }
602
603 $file = $ref->getFileName();
604 if ( ! $file ) {
605 // Internal/built-in callback (no source file) — not ours.
606 return false;
607 }
608
609 return strpos( wp_normalize_path( $file ), $plugin_dir ) === 0;
610 }
611
612 #---------fixes issue : #95 ----------
613 #This function is to redirect in case client changes slug on fresh install
614 #It is called by the add_option hook
615
616 public function redirect_slug_for_freshinstalls(){
617 #get the db menu slug
618 $plugin_menu_slug = Metasync::get_option('general')['white_label_plugin_menu_slug'] ?? '';
619
620 #check that the slug is set or set it to the defaul class slug usually ('searchatlas')
621 $current_slug = empty($plugin_menu_slug) ? self::$page_slug : $plugin_menu_slug;
622
623 #check if we have the cookie set and check if the slug has changed
624 if (isset($_COOKIE['metasync_previous_slug']) && $_COOKIE['metasync_previous_slug'] !== $current_slug) {
625 # the slug changed so we need to update the cookie
626 setcookie('metasync_previous_slug', $current_slug, [
627 'expires' => time() + 3600,
628 'path' => COOKIEPATH,
629 'domain' => COOKIE_DOMAIN,
630 'secure' => is_ssl(),
631 'httponly' => true,
632 'samesite' => 'Lax',
633 ]);
634 $_COOKIE['metasync_previous_slug'] = $current_slug;
635
636 #Redirect url to the new slug
637 $redirect_url = admin_url('admin.php?page=' . $current_slug);
638
639 wp_redirect($redirect_url);
640 exit;
641 }
642 }
643
644 public function metasync_plugin_updated_action($upgrader_object, $options){
645 if ($options['action'] == 'update' && $options['type'] == 'plugin') {
646 // List of plugins being updated
647 $updated_plugins = $options['plugins'];
648
649 // Loop through plugins and check if your plugin is updated
650 if (in_array(plugin_basename(dirname(__DIR__) . '/metasync.php'), $updated_plugins, true)) {
651 // Only set debug options if they don't already exist (first install/update)
652 if (get_option('wp_debug_enabled') === false) {
653 update_option('wp_debug_enabled', 'true');
654 }
655 if (get_option('wp_debug_log_enabled') === false) {
656 update_option('wp_debug_log_enabled', 'true');
657 }
658 if (get_option('wp_debug_display_enabled') === false) {
659 update_option('wp_debug_display_enabled', 'false');
660 }
661 }
662 }
663 }
664
665 public function initialize_cookie() {
666 // Check if cookie is already set
667 if (!isset($_COOKIE['metasync_previous_slug'])) {
668 // Only set cookie if headers haven't been sent yet
669 if (!headers_sent()) {
670 $data = Metasync::get_option('general');
671 // Retrieve the current slug
672 $initial_slug = isset($data['white_label_plugin_menu_slug']) ? $data['white_label_plugin_menu_slug'] : self::$page_slug;
673 // Set the cookie
674 setcookie('metasync_previous_slug', $initial_slug, [
675 'expires' => time() + 3600,
676 'path' => COOKIEPATH,
677 'domain' => COOKIE_DOMAIN,
678 'secure' => is_ssl(),
679 'httponly' => true,
680 'samesite' => 'Lax',
681 ]);
682 }
683 }
684 }
685
686 /**
687 * Redirect to setup wizard on first activation
688 *
689 * @since 1.0.0
690 */
691 public function maybe_redirect_to_wizard() {
692 // Only redirect if wizard should be shown
693 if (get_option('metasync_show_wizard') && !isset($_GET['page'])) {
694 delete_option('metasync_show_wizard');
695
696 // Don't redirect during AJAX, cron, or bulk activation
697 if (wp_doing_ajax() || wp_doing_cron() || isset($_GET['activate-multi'])) {
698 return;
699 }
700
701 // Only redirect if user has access to the plugin
702 if (!Metasync::current_user_has_plugin_access()) {
703 return;
704 }
705
706 wp_safe_redirect(admin_url('admin.php?page=' . self::$page_slug . '-setup-wizard'));
707 exit;
708 }
709 }
710
711 public function metasync_display_error_log() {
712 Metasync_Debug_Manager::instance()->metasync_display_error_log($this);
713 }
714 public function metasync_update_wp_config() {
715 Metasync_Debug_Manager::instance()->metasync_update_wp_config();
716 }
717
718
719 /**
720 * Sync plugin file headers when metasync_options is updated.
721 * This ensures whitelabel values are written to the plugin file header
722 * so they persist even when the plugin is deactivated.
723 *
724 * @param mixed $old_value The old option value.
725 * @param mixed $new_value The new option value.
726 * @since 2.5.0
727 */
728 public function on_options_updated_sync_file_headers($old_value, $new_value)
729 {
730 Metasync_Debug_Manager::instance()->on_options_updated_sync_file_headers($old_value, $new_value);
731 }
732
733 public function check_and_redirect_slug($option, $old_value, $new_value) {
734 // Ensure this hook is only triggered for your specific option group
735
736 if (!isset($option['general'] ) && !isset($option['general']['white_label_plugin_menu_slug'])) {
737
738 return;
739 }
740
741 $new_slug = $new_value['general']['white_label_plugin_menu_slug'] ?? self::$page_slug;
742
743 $old_slug = $old_value['general']['white_label_plugin_menu_slug'] ?? self::$page_slug;
744
745 if ($new_slug !== $old_slug && $old_slug !=='' ){
746 // Set a new cookie
747 setcookie('metasync_previous_slug', $new_slug, [
748 'expires' => time() + 3600,
749 'path' => COOKIEPATH,
750 'domain' => COOKIE_DOMAIN,
751 'secure' => is_ssl(),
752 'httponly' => true,
753 'samesite' => 'Lax',
754 ]);
755 $_COOKIE['metasync_previous_slug'] = $new_slug;
756 // Redirect to the new slug
757 $redirect_url = admin_url('admin.php?page=' . $old_slug);
758 wp_redirect($redirect_url);
759 exit;
760 }else{
761 self::$page_slug = Metasync::get_option('general')['white_label_plugin_menu_slug']=="" ? "searchatlas":Metasync::get_option('general')['white_label_plugin_menu_slug'];
762 $redirect_url = admin_url('admin.php?page=' . self::$page_slug);
763
764 #add redirection for when the old slug is not defined
765 #this fixes the redirect issue #
766 wp_redirect($redirect_url);
767 exit;
768 }
769 }
770 public function metasync_view_detials_url( $plugin_meta, $plugin_file, $plugin_data ) {
771 $plugin_uri = Metasync::get_option('general')['white_label_plugin_uri'] ?? '';
772 $this_plugin = plugin_basename(dirname(__DIR__) . '/metasync.php');
773 if ($this_plugin === $plugin_file && $plugin_uri !== '') {
774 foreach ($plugin_meta as &$meta) {
775 if (strpos($meta, 'open-plugin-details-modal') !== false) {
776 $meta = sprintf(
777 '<a href="%s" class="thickbox open-plugin-details-modal" aria-label="%s" data-title="%s">%s</a>',
778 add_query_arg('TB_iframe', 'true', $plugin_uri),
779 esc_attr(sprintf(esc_html__('More information about %s'), $plugin_data['Name'])),
780 esc_attr($plugin_data['Name']),
781 esc_html__('View details', 'metasync')
782 );
783 break; // Exit loop after replacing the link
784 }
785 }
786 }
787 return $plugin_meta;
788 }
789
790 public function metasync_plugin_white_label($all_plugins) {
791 $general = Metasync::get_option('general');
792 if (!is_array($general)) {
793 return $all_plugins;
794 }
795
796 $plugin_name = $general['white_label_plugin_name'] ?? '';
797 $plugin_description = $general['white_label_plugin_description'] ?? '';
798 $plugin_author = $general['white_label_plugin_author'] ?? '';
799 $plugin_author_uri = $general['white_label_plugin_author_uri'] ?? '';
800 $plugin_uri = $general['white_label_plugin_uri'] ?? '';
801
802 // Dynamically resolve the plugin basename to handle renamed plugin folders
803 $this_plugin = plugin_basename(dirname(__DIR__) . '/metasync.php');
804
805 if (isset($all_plugins[$this_plugin])) {
806 if ($plugin_name !== '') {
807 $all_plugins[$this_plugin]['Name'] = $plugin_name;
808 $all_plugins[$this_plugin]['Title'] = $plugin_name;
809 }
810 if ($plugin_description !== '') {
811 $all_plugins[$this_plugin]['Description'] = $plugin_description;
812 }
813 if ($plugin_author !== '') {
814 $all_plugins[$this_plugin]['Author'] = $plugin_author;
815 $all_plugins[$this_plugin]['AuthorName'] = $plugin_author;
816 }
817 if ($plugin_author_uri !== '') {
818 $all_plugins[$this_plugin]['AuthorURI'] = $plugin_author_uri;
819 }
820 if ($plugin_uri !== '') {
821 $all_plugins[$this_plugin]['PluginURI'] = $plugin_uri;
822 }
823 }
824
825 return $all_plugins;
826 }
827
828 /**
829 * Inject the whitelabel icon into the update_plugins transient so that
830 * /wp-admin/update-core.php (Dashboard → Updates) shows the WL icon
831 * instead of the SearchAtlas icon returned by the update API.
832 *
833 * @param object $transient The site transient object.
834 * @return object
835 */
836 public function inject_whitelabel_icon_into_update_transient($transient) {
837 if (empty($transient) || !is_object($transient)) {
838 return $transient;
839 }
840
841 $general = Metasync::get_option('general');
842 if (!is_array($general)) {
843 return $transient;
844 }
845
846 $icon_url = $general['white_label_plugin_menu_icon'] ?? '';
847 if (empty($icon_url)) {
848 return $transient;
849 }
850
851 $this_plugin = plugin_basename(dirname(__DIR__) . '/metasync.php');
852
853 // Inject into pending updates list
854 if (!empty($transient->response) && isset($transient->response[$this_plugin])) {
855 $transient->response[$this_plugin]->icons = [
856 '1x' => $icon_url,
857 '2x' => $icon_url,
858 ];
859 }
860
861 // Also inject into the "no update needed" list so the icon appears
862 // on the updates screen even when the plugin is up-to-date
863 if (!empty($transient->no_update) && isset($transient->no_update[$this_plugin])) {
864 $transient->no_update[$this_plugin]->icons = [
865 '1x' => $icon_url,
866 '2x' => $icon_url,
867 ];
868 }
869
870 return $transient;
871 }
872
873 /**
874 * Register the stylesheets for the admin area.
875 *
876 * @since 1.0.0
877 */
878 public function enqueue_styles()
879 {
880 $current_page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : '';
881
882 if (strpos($current_page, self::$page_slug) === 0) {
883 wp_enqueue_style(
884 $this->plugin_name,
885 plugin_dir_url(__FILE__) . 'css/metasync-admin.css',
886 array(),
887 $this->version,
888 'all'
889 );
890
891 // Enqueue dashboard-style CSS for admin pages
892 wp_enqueue_style(
893 $this->plugin_name . '-dashboard',
894 plugin_dir_url(__FILE__) . 'css/metasync-dashboard.css',
895 array($this->plugin_name),
896 $this->version,
897 'all'
898 );
899
900 // Enqueue 3-column layout CSS
901 wp_enqueue_style(
902 $this->plugin_name . '-layout',
903 plugin_dir_url(__FILE__) . 'css/metasync-layout.css',
904 array($this->plugin_name . '-dashboard'),
905 $this->version,
906 'all'
907 );
908 }
909
910 // Enqueue wizard CSS if on wizard page
911 if (isset($_GET['page']) && strpos($_GET['page'], '-setup-wizard') !== false) {
912 wp_enqueue_style(
913 $this->plugin_name . '-setup-wizard',
914 plugin_dir_url(__FILE__) . 'css/metasync-setup-wizard.css',
915 array($this->plugin_name . '-dashboard'),
916 $this->version,
917 'all'
918 );
919 }
920
921 // Enqueue SEO Health CSS if on the SEO Health page
922 if (isset($_GET['page']) && strpos($_GET['page'], '-seo-health') !== false) {
923 wp_enqueue_style(
924 $this->plugin_name . '-seo-health',
925 plugin_dir_url(__FILE__) . 'css/metasync-seo-health.css',
926 array($this->plugin_name . '-dashboard'),
927 $this->version,
928 'all'
929 );
930 }
931 }
932
933 /**
934 * Register the JavaScript for the admin area.
935 *
936 * @since 1.0.0
937 */
938 public function enqueue_scripts()
939 {
940 // --- Phase 5 (#887): Extracted inline JS files ---
941 $current_page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : '';
942 $plugin_root_url = plugin_dir_url(dirname(__FILE__));
943 $is_metasync_page = ($current_page === self::$page_slug || strpos($current_page, self::$page_slug) === 0);
944
945 if ($is_metasync_page) {
946 wp_enqueue_media();
947
948 wp_enqueue_script(
949 $this->plugin_name,
950 plugin_dir_url(__FILE__) . 'js/metasync-admin.js',
951 array('jquery'),
952 $this->version,
953 false
954 );
955
956 // Enqueue dashboard-style JavaScript for enhanced interactions
957 wp_enqueue_script(
958 $this->plugin_name . '-dashboard',
959 plugin_dir_url(__FILE__) . 'js/metasync-dashboard.js',
960 array('jquery', $this->plugin_name),
961 $this->version,
962 true
963 );
964
965 # Enqueue theme switcher
966 wp_enqueue_script(
967 $this->plugin_name . '-theme-switcher',
968 plugin_dir_url(__FILE__) . 'js/metasync-theme-switcher.js',
969 array('jquery'),
970 $this->version,
971 true
972 );
973
974 // Keeps core's admin-menu pinning in step with pages whose height
975 // changes after load. Depends on 'common' so core has bound its
976 // own handlers before this runs.
977 wp_enqueue_script(
978 $this->plugin_name . '-admin-menu-height',
979 plugin_dir_url(__FILE__) . 'js/metasync-admin-menu-height.js',
980 array('jquery', 'common'),
981 $this->version,
982 true
983 );
984 }
985
986 $per_page_screens = [
987 self::$page_slug . '-redirections',
988 self::$page_slug . '-404-monitor',
989 self::$page_slug . '-sync-log',
990 self::$page_slug . '-media-optimization',
991 ];
992
993 if (in_array($current_page, $per_page_screens, true)) {
994 // Results-per-page selectors use this external handler instead of
995 // an inline event attribute so changing a page size always reloads
996 // the current admin page.
997 wp_enqueue_script(
998 $this->plugin_name . '-per-page',
999 plugin_dir_url(__FILE__) . 'js/metasync-per-page.js',
1000 array(),
1001 $this->version,
1002 true
1003 );
1004 }
1005
1006 // Dashboard iframe height (only on dashboard page)
1007 if ($current_page === self::$page_slug . '-dashboard' || $current_page === self::$page_slug) {
1008 wp_enqueue_script(
1009 $this->plugin_name . '-iframe',
1010 plugin_dir_url(__FILE__) . 'js/metasync-iframe.js',
1011 array(),
1012 $this->version,
1013 false // Load in head — needed before iframe renders
1014 );
1015 }
1016
1017 // Settings page scripts (save btn, clear settings, bing key, access roles)
1018 if ($current_page === self::$page_slug) {
1019 wp_enqueue_script(
1020 $this->plugin_name . '-settings',
1021 plugin_dir_url(__FILE__) . 'js/metasync-settings.js',
1022 array('jquery'),
1023 $this->version,
1024 true
1025 );
1026 }
1027
1028 // Tab switcher (redirections / 404-monitor page)
1029 if ($current_page === self::$page_slug . '-redirections') {
1030 wp_enqueue_script(
1031 $this->plugin_name . '-tab-switcher',
1032 plugin_dir_url(__FILE__) . 'js/metasync-tab-switcher.js',
1033 array('jquery'),
1034 $this->version,
1035 true
1036 );
1037 }
1038
1039 // Sitemap tabs (xml-sitemap page)
1040 if ($current_page === self::$page_slug . '-xml-sitemap') {
1041 wp_enqueue_style(
1042 $this->plugin_name . '-sitemap-tabs',
1043 plugin_dir_url(__FILE__) . 'css/metasync-sitemap-tabs.css',
1044 array($this->plugin_name . '-dashboard'),
1045 $this->version
1046 );
1047 wp_enqueue_script(
1048 $this->plugin_name . '-sitemap-tabs',
1049 plugin_dir_url(__FILE__) . 'js/metasync-sitemap-tabs.js',
1050 array('jquery'),
1051 $this->version,
1052 true
1053 );
1054 // Pass the active tab from server (handles POST redirect)
1055 $sitemap_active_tab = 'general';
1056 if (isset($_GET['tab']) && in_array($_GET['tab'], ['general', 'news', 'video'], true)) {
1057 $sitemap_active_tab = sanitize_text_field(wp_unslash($_GET['tab']));
1058 } elseif (isset($_POST['redirect_tab']) && in_array($_POST['redirect_tab'], ['general', 'news', 'video'], true)) {
1059 $sitemap_active_tab = sanitize_text_field(wp_unslash($_POST['redirect_tab']));
1060 }
1061 wp_localize_script($this->plugin_name . '-sitemap-tabs', 'metasyncSitemapTabs', [
1062 'activeTab' => $sitemap_active_tab,
1063 ]);
1064 }
1065
1066 // Error logs — copy to clipboard
1067 if ($current_page === self::$page_slug) {
1068 wp_enqueue_script(
1069 $this->plugin_name . '-error-logs',
1070 $plugin_root_url . 'site-error-logs/js/metasync-error-logs.js',
1071 array(),
1072 $this->version,
1073 true
1074 );
1075 }
1076
1077 // Access control UI
1078 if ($current_page === self::$page_slug) {
1079 wp_enqueue_script(
1080 $this->plugin_name . '-access-control',
1081 $plugin_root_url . 'includes/js/metasync-access-control.js',
1082 array(),
1083 $this->version,
1084 true
1085 );
1086 }
1087
1088 // 404 monitor filter
1089 if ($current_page === self::$page_slug . '-redirections' || $current_page === self::$page_slug . '-404-monitor') {
1090 wp_enqueue_script(
1091 $this->plugin_name . '-404-monitor',
1092 $plugin_root_url . 'views/js/metasync-404-monitor.js',
1093 array(),
1094 $this->version,
1095 true
1096 );
1097 }
1098
1099 // Redirections filter
1100 if ($current_page === self::$page_slug . '-redirections') {
1101 wp_enqueue_script(
1102 $this->plugin_name . '-redirections',
1103 $plugin_root_url . 'views/js/metasync-redirections.js',
1104 array(),
1105 $this->version,
1106 true
1107 );
1108 wp_localize_script($this->plugin_name . '-redirections', 'metasyncHealthCheck', array(
1109 'ajaxUrl' => admin_url('admin-ajax.php'),
1110 'healthNonce' => wp_create_nonce('metasync_redirect_health_check'),
1111 ));
1112 }
1113 // --- Phase 5 Part B: Extracted inline JS with wp_localize_script ---
1114
1115 // Navigation portal menus (top bar + settings inner page)
1116 $whitelabel_data = Metasync::get_option('whitelabel');
1117 if ($is_metasync_page) {
1118 wp_enqueue_script(
1119 $this->plugin_name . '-navigation',
1120 plugin_dir_url(__FILE__) . 'js/metasync-navigation.js',
1121 array(),
1122 $this->version,
1123 false // Load in head — global functions called from onclick attributes
1124 );
1125 wp_localize_script($this->plugin_name . '-navigation', 'metasyncNavData', array(
1126 'hideAdvanced' => !empty($whitelabel_data['hide_advanced']),
1127 'showGeneral' => Metasync_Access_Control::user_can_access('hide_settings'),
1128 'pageSlug' => self::$page_slug,
1129 ));
1130 }
1131
1132 // Debug mode timer
1133 if ($current_page === self::$page_slug) {
1134 $debug_manager = Metasync_Debug_Mode_Manager::get_instance();
1135 $debug_status = $debug_manager->get_status();
1136 wp_enqueue_script(
1137 $this->plugin_name . '-debug-mode',
1138 plugin_dir_url(__FILE__) . 'js/metasync-debug-mode.js',
1139 array('jquery'),
1140 $this->version,
1141 true
1142 );
1143 wp_localize_script($this->plugin_name . '-debug-mode', 'metasyncDebugData', array(
1144 'enabled' => !empty($debug_status['enabled']),
1145 'indefinite' => !empty($debug_status['indefinite']),
1146 'timeRemaining' => isset($debug_status['time_remaining']) ? (int) $debug_status['time_remaining'] : 0,
1147 'statusUrl' => rest_url('metasync/v1/debug-mode/status'),
1148 'restNonce' => wp_create_nonce('wp_rest'),
1149 ));
1150 }
1151
1152 // MetasyncConfig + admin bar sync
1153 if ($is_metasync_page) {
1154 wp_enqueue_script(
1155 $this->plugin_name . '-config',
1156 plugin_dir_url(__FILE__) . 'js/metasync-config.js',
1157 array('jquery'),
1158 $this->version,
1159 true
1160 );
1161 wp_localize_script($this->plugin_name . '-config', 'metasyncConfigData', array(
1162 'pluginName' => Metasync::get_effective_plugin_name(),
1163 'ottoName' => Metasync::get_whitelabel_otto_name(),
1164 ));
1165 }
1166
1167 // Whitelabel password (forgot password handler)
1168 if ($current_page === self::$page_slug) {
1169 wp_enqueue_script(
1170 $this->plugin_name . '-whitelabel',
1171 plugin_dir_url(__FILE__) . 'js/metasync-whitelabel.js',
1172 array('jquery'),
1173 $this->version,
1174 true
1175 );
1176 wp_localize_script($this->plugin_name . '-whitelabel', 'metasyncWhitelabelData', array(
1177 'recoverNonce' => wp_create_nonce('metasync_recover_password_nonce'),
1178 ));
1179 }
1180
1181 // Whitelabel connect (validation modal + lock section + export)
1182 if ($current_page === self::$page_slug) {
1183 wp_enqueue_script(
1184 $this->plugin_name . '-connect',
1185 plugin_dir_url(__FILE__) . 'js/metasync-connect.js',
1186 array('jquery'),
1187 $this->version,
1188 true
1189 );
1190 wp_localize_script($this->plugin_name . '-connect', 'metasyncConnectData', array(
1191 'optionKey' => self::option_key,
1192 'adminPostUrl' => admin_url('admin-post.php'),
1193 'exportNonce' => wp_create_nonce('metasync_export_whitelabel'),
1194 'ajaxUrl' => admin_url('admin-ajax.php'),
1195 'logoutNonceField' => wp_nonce_field('whitelabel_logout_nonce', 'whitelabel_logout_nonce', true, false),
1196 ));
1197 }
1198
1199 // Host blocking test (settings + dashboard)
1200 if ($current_page === self::$page_slug || $current_page === self::$page_slug . '-dashboard') {
1201 wp_enqueue_script(
1202 $this->plugin_name . '-host-blocking',
1203 plugin_dir_url(__FILE__) . 'js/metasync-host-blocking.js',
1204 array('jquery'),
1205 $this->version,
1206 true
1207 );
1208 wp_localize_script($this->plugin_name . '-host-blocking', 'metasyncHostBlockingData', array(
1209 'ajaxUrl' => admin_url('admin-ajax.php'),
1210 'nonce' => wp_create_nonce('metasync_nonce'),
1211 ));
1212 }
1213
1214 // OTTO excluded URLs (lives on the Compatibility page)
1215 if ($current_page === self::$page_slug . '-compatibility') {
1216 wp_enqueue_script(
1217 $this->plugin_name . '-excluded-urls',
1218 plugin_dir_url(__FILE__) . 'js/metasync-excluded-urls.js',
1219 array('jquery'),
1220 $this->version,
1221 true
1222 );
1223 wp_localize_script($this->plugin_name . '-excluded-urls', 'metasyncExcludedUrlsData', array(
1224 'ajaxUrl' => admin_url('admin-ajax.php'),
1225 'nonce' => wp_create_nonce('metasync_otto_excluded_urls'),
1226 ));
1227 }
1228
1229 // Execution settings form
1230 if ($current_page === self::$page_slug) {
1231 $server_limits = $this->get_server_limits();
1232 wp_enqueue_script(
1233 $this->plugin_name . '-execution-settings',
1234 plugin_dir_url(__FILE__) . 'js/metasync-execution-settings.js',
1235 array('jquery'),
1236 $this->version,
1237 true
1238 );
1239 wp_localize_script($this->plugin_name . '-execution-settings', 'metasyncExecSettingsData', array(
1240 'serverMaxExecTime' => ($server_limits['max_execution_time_raw'] == -1) ? 'Infinity' : (int) $server_limits['max_execution_time_raw'],
1241 'serverMaxMemory' => ($server_limits['memory_limit_raw'] == -1) ? 'Infinity' : (int) $server_limits['memory_limit_raw'],
1242 'canChangeMemory' => !empty($server_limits['can_change_memory']),
1243 ));
1244 }
1245
1246 // Quick edit badge (custom pages)
1247 global $pagenow;
1248 if ($pagenow === 'edit.php') {
1249 wp_enqueue_script(
1250 $this->plugin_name . '-quick-edit',
1251 plugin_dir_url(__FILE__) . 'js/metasync-quick-edit.js',
1252 array('jquery'),
1253 $this->version,
1254 true
1255 );
1256 wp_localize_script($this->plugin_name . '-quick-edit', 'metasyncQuickEditData', array(
1257 'standardPageLabel' => __('Standard page', 'metasync'),
1258 ));
1259 }
1260
1261 // Report issue form
1262 if ($current_page === self::$page_slug . '-report-issue') {
1263 wp_enqueue_script(
1264 $this->plugin_name . '-report-issue',
1265 $plugin_root_url . 'views/js/metasync-report-issue.js',
1266 array('jquery'),
1267 $this->version,
1268 true
1269 );
1270 }
1271
1272 // Bing console
1273 if ($current_page === self::$page_slug . '-bing-console') {
1274 wp_enqueue_script(
1275 $this->plugin_name . '-bing-console',
1276 $plugin_root_url . 'views/js/metasync-bing-console.js',
1277 array('jquery'),
1278 $this->version,
1279 true
1280 );
1281 wp_localize_script($this->plugin_name . '-bing-console', 'metasyncBingConsoleData', array(
1282 'nonce' => wp_create_nonce('metasync_nonce'),
1283 ));
1284 }
1285
1286 // Add redirection form
1287 if ($current_page === self::$page_slug . '-redirections') {
1288 wp_enqueue_script(
1289 $this->plugin_name . '-add-redirection',
1290 $plugin_root_url . 'views/js/metasync-add-redirection.js',
1291 array(),
1292 $this->version,
1293 true
1294 );
1295 }
1296
1297 // Import redirections
1298 if ($current_page === self::$page_slug . '-redirections') {
1299 wp_enqueue_script(
1300 $this->plugin_name . '-import-redirections',
1301 $plugin_root_url . 'views/js/metasync-import-redirections.js',
1302 array('jquery'),
1303 $this->version,
1304 true
1305 );
1306 wp_localize_script($this->plugin_name . '-import-redirections', 'metasyncImportRedirData', array(
1307 'nonce' => wp_create_nonce('metasync_import_redirections'),
1308 'redirectUrl' => admin_url('admin.php?page=' . self::$page_slug . '-redirections'),
1309 ));
1310 }
1311
1312 // Import external data (SEO metadata)
1313 if ($current_page === self::$page_slug . '-import-external') {
1314 wp_enqueue_script(
1315 $this->plugin_name . '-import-external-data',
1316 $plugin_root_url . 'views/js/metasync-import-external-data.js',
1317 array('jquery'),
1318 $this->version,
1319 true
1320 );
1321 wp_localize_script($this->plugin_name . '-import-external-data', 'metasyncImportData', array(
1322 'importNonce' => wp_create_nonce('metasync_import_external_data'),
1323 'seoImportNonce' => wp_create_nonce('metasync_import_seo_metadata'),
1324 ));
1325 }
1326
1327 // OTTO bot statistics
1328 if ($current_page === self::$page_slug . '-bot-statistics') {
1329 wp_enqueue_script(
1330 $this->plugin_name . '-bot-statistics',
1331 $plugin_root_url . 'views/js/metasync-bot-statistics.js',
1332 array('jquery'),
1333 $this->version,
1334 true
1335 );
1336 wp_localize_script($this->plugin_name . '-bot-statistics', 'metasyncBotStatsData', array(
1337 'resetNonce' => wp_create_nonce('metasync_reset_bot_stats'),
1338 ));
1339 }
1340
1341 // OTTO debug page
1342 if ($current_page === self::$page_slug . '-otto-debug') {
1343 wp_enqueue_script(
1344 $this->plugin_name . '-otto-debug',
1345 plugin_dir_url(__FILE__) . 'js/metasync-otto-debug.js',
1346 array('jquery'),
1347 $this->version,
1348 true
1349 );
1350 wp_localize_script($this->plugin_name . '-otto-debug', 'metasyncOttoDebugData', array(
1351 'nonce' => wp_create_nonce('metasync_otto_debug'),
1352 ));
1353 }
1354
1355 // --- End Phase 5 enqueues ---
1356
1357 if ($is_metasync_page) {
1358 # Localize theme switcher script
1359 wp_localize_script(
1360 $this->plugin_name . '-theme-switcher',
1361 'metasyncThemeData',
1362 array(
1363 'ajaxUrl' => admin_url('admin-ajax.php'),
1364 'nonce' => wp_create_nonce('metasync_theme_nonce'),
1365 'currentTheme' => get_option('metasync_theme', 'dark')
1366 )
1367 );
1368
1369 // Get connection status for JavaScript
1370 $general_settings = Metasync::get_option('general');
1371 $searchatlas_api_key = isset($general_settings['searchatlas_api_key']) ? $general_settings['searchatlas_api_key'] : '';
1372 $otto_pixel_uuid = isset($general_settings['otto_pixel_uuid']) ? $general_settings['otto_pixel_uuid'] : '';
1373
1374 // SECURITY FIX (CVE-2025-14386): Only generate Search Atlas connect nonce for administrators
1375 // Using strict capability check instead of plugin access roles
1376 $sa_connect_nonce = '';
1377 if (current_user_can('manage_options')) {
1378 $sa_connect_nonce = wp_create_nonce('metasync_sa_connect_nonce');
1379 }
1380
1381 $heartbeat_state = $this->get_heartbeat_state();
1382 wp_localize_script( $this->plugin_name, 'metaSync', array(
1383 'ajax_url' => admin_url( 'admin-ajax.php' ),
1384 'admin_url'=>admin_url('admin.php'),
1385 'nonce' => wp_create_nonce('metasync_nonce'),
1386 'sa_connect_nonce' => $sa_connect_nonce,
1387 'reset_auth_nonce' => wp_create_nonce('metasync_reset_auth_nonce'),
1388 'burst_ping_nonce' => wp_create_nonce('metasync_burst_ping'),
1389 'heartbeat_state' => $heartbeat_state,
1390 'dashboard_domain' => self::get_effective_dashboard_domain(),
1391 'support_email' => Metasync::SUPPORT_EMAIL,
1392 'documentation_domain' => Metasync::DOCUMENTATION_DOMAIN,
1393 'debug_enabled' => WP_DEBUG || (defined('METASYNC_DEBUG') && constant('METASYNC_DEBUG')),
1394 'searchatlas_api_key' => !empty($searchatlas_api_key),
1395 'otto_pixel_uuid' => $otto_pixel_uuid,
1396 'is_connected' => (bool)$this->is_heartbeat_connected()
1397 ));
1398
1399 // Ensure ajaxurl is available for admin pages (WordPress standard)
1400 // This creates a global ajaxurl variable for JavaScript
1401 wp_enqueue_script('wp-util');
1402
1403 // Add inline script to ensure ajaxurl is defined
1404 $inline_script = "
1405 if (typeof ajaxurl === 'undefined') {
1406 var ajaxurl = '" . esc_js(admin_url('admin-ajax.php')) . "';
1407 }
1408
1409 // Add Plugin Auth Token refresh functionality
1410 jQuery(document).ready(function($) {
1411 $('#refresh-plugin-auth-token').click(function() {
1412 var button = $(this);
1413 var originalText = button.text();
1414
1415 if (confirm('Are you sure you want to refresh the Plugin Auth Token? This will generate a new token and update the heartbeat API.')) {
1416 // Disable button and show loading
1417 button.prop('disabled', true).text('🔄 Refreshing...');
1418
1419 $.post(ajaxurl, {
1420 action: 'metasync_refresh_plugin_auth_token',
1421 nonce: '" . wp_create_nonce('metasync_refresh_plugin_auth_token') . "'
1422 })
1423 .done(function(response) {
1424 if (response.success && response.data && response.data.new_token) {
1425 // Update the field value immediately
1426 $('#apikey').val(response.data.new_token);
1427
1428 // Visual feedback with green border
1429 $('#apikey').css('border', '2px solid #28a745').animate({borderColor: '#ddd'}, 2000);
1430
1431 alert('�
1432 Plugin Auth Token refreshed successfully!\\n\\nNew token: ' + response.data.new_token.substring(0, 8) + '...');
1433 } else {
1434 alert('❌ Error refreshing token: ' + (response.data ? response.data.message : 'Unknown error'));
1435 }
1436 })
1437 .fail(function() {
1438 alert('❌ Network error while refreshing token');
1439 })
1440 .always(function() {
1441 // Re-enable button
1442 button.prop('disabled', false).text(originalText);
1443 });
1444 }
1445 });
1446 });
1447 ";
1448 wp_add_inline_script($this->plugin_name, $inline_script);
1449 }
1450 add_action('admin_notices', array($this, 'permalink_structure_dashboard_warning'));
1451 add_action('admin_notices', array($this, 'display_page_builder_notice'));
1452 // Display update warning banner if plugin update is available
1453 add_action('admin_notices', array($this, 'display_update_warning_banner'));
1454 // Enqueue wizard assets if on wizard page
1455 if (isset($_GET['page']) && strpos($_GET['page'], '-setup-wizard') !== false) {
1456 wp_enqueue_script(
1457 $this->plugin_name . '-setup-wizard',
1458 plugin_dir_url(__FILE__) . 'js/metasync-setup-wizard.js',
1459 array('jquery'),
1460 $this->version,
1461 true
1462 );
1463
1464 wp_localize_script($this->plugin_name . '-setup-wizard', 'metasyncWizardData', array(
1465 'nonce' => wp_create_nonce('metasync_wizard'),
1466 'ssoNonce' => wp_create_nonce('metasync_sso_nonce'),
1467 'saConnectNonce' => wp_create_nonce('metasync_sa_connect_nonce'),
1468 'importNonce' => wp_create_nonce('metasync_import_external_data'),
1469 'dashboardUrl' => admin_url('admin.php?page=' . self::$page_slug . '-dashboard'),
1470 'currentStep' => 1,
1471 'totalSteps' => 6,
1472 'pluginName' => Metasync::get_effective_plugin_name()
1473 ));
1474 }
1475
1476 wp_enqueue_script('heartbeat');
1477 }
1478
1479 /**
1480 * Settings of HeartBeat API for admin area.
1481 *
1482 * The only thing the plugin rides the beat for is a periodic admin
1483 * telemetry ping (the `heartbeat-send` handler in admin/js/metasync-admin.js),
1484 * so it slows the beat down rather than speeding it up. Two contexts are
1485 * left on whatever core decided, because core leans on the beat there:
1486 *
1487 * - The front end, where the beat drives wp-auth-check — the "your session
1488 * has expired, log in again" prompt.
1489 * - The editor screens, where each tick renews the post lock. A lock only
1490 * lives for wp_check_post_lock_window() seconds (150 by default), so any
1491 * interval longer than that drops the lock between beats and lets a
1492 * second editor silently take over a post someone still has open.
1493 *
1494 * Core clamps this to 1-3600 seconds in wp-includes/js/heartbeat.js, so an
1495 * over-long interval is not corrected on our behalf.
1496 *
1497 * @param array $settings Heartbeat settings array.
1498 * @return array Heartbeat settings array.
1499 */
1500 function metasync_heartbeat_settings($settings)
1501 {
1502 if (!is_admin()) {
1503 return $settings;
1504 }
1505
1506 # $pagenow is set in wp-includes/vars.php, long before this filter runs;
1507 # core's own wp_heartbeat_set_suspension() reads it exactly this way.
1508 global $pagenow;
1509 $lock_dependent_screens = array('post.php', 'post-new.php', 'customize.php');
1510 if (isset($pagenow) && in_array($pagenow, $lock_dependent_screens, true)) {
1511 return $settings;
1512 }
1513
1514 # 60s matches heartbeat.js's own default and stays well inside the
1515 # 150s post-lock window, so nothing core does can fall through a gap.
1516 $settings['interval'] = 60;
1517 return $settings;
1518 }
1519
1520 /**
1521 * Data or Response received from HeartBeat API for admin area.
1522 */
1523 function metasync_received_data($response, $data)
1524 {
1525 // if ($data['client'] == 'marco')
1526
1527 $response['server'] = wp_json_encode($data);
1528
1529 return $response;
1530 }
1531
1532 /**
1533 * Add Import External Data page
1534 */
1535 public function add_import_external_data_page()
1536 {
1537 # Check if current user has plugin access based on role settings
1538 if (!$this->current_user_has_plugin_access()) {
1539 return; // Don't add this page for users without access
1540 }
1541
1542 // Use 'read' capability since actual access is controlled by current_user_has_plugin_access() check above
1543 add_submenu_page(
1544 '', // Hidden from menu, linked from other pages
1545 'Import External Data',
1546 'Import External Data',
1547 'read',
1548 self::$page_slug . '-import-external',
1549 array($this, 'render_import_external_data_page')
1550 );
1551 }
1552
1553 /**
1554 * Render Import External Data page
1555 */
1556 public function render_import_external_data_page()
1557 {
1558 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-external-importer.php';
1559 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-import-external-data.php';
1560 }
1561
1562 /**
1563 * AJAX handler for external data import
1564 */
1565 public function ajax_import_external_data()
1566 {
1567 Metasync_Admin_Ajax::instance()->ajax_import_external_data();
1568 }
1569
1570 /**
1571 * AJAX handler for SEO metadata batch import
1572 * Supports batch processing with progress tracking
1573 */
1574 public function ajax_import_seo_metadata()
1575 {
1576 Metasync_Admin_Ajax::instance()->ajax_import_seo_metadata();
1577 }
1578
1579 /**
1580 * Additional context validation for Search Atlas connect tokens.
1581 *
1582 * Validates site URL and optional IP/user-agent context for connect tokens
1583 * used during the Search Atlas API key + Otto UUID retrieval flow.
1584 * This does NOT create WordPress login sessions.
1585 */
1586 private function validate_searchatlas_context($token_data)
1587 {
1588 return Metasync_Connect_Manager::instance()->validate_searchatlas_context($token_data);
1589 }
1590
1591 private function should_validate_ip()
1592 {
1593 return Metasync_Connect_Manager::instance()->should_validate_ip();
1594 }
1595
1596 private function are_user_agents_incompatible($old_ua, $new_ua)
1597 {
1598 return Metasync_Connect_Manager::instance()->are_user_agents_incompatible($old_ua, $new_ua);
1599 }
1600
1601 private function extract_browser_name($ua)
1602 {
1603 return Metasync_Connect_Manager::instance()->extract_browser_name($ua);
1604 }
1605
1606 public function generate_searchatlas_wp_connect_token($regenerate = false)
1607 {
1608 return Metasync_Connect_Manager::instance()->generate_searchatlas_wp_connect_token($regenerate);
1609 }
1610
1611 private function ensure_plugin_auth_token_exists()
1612 {
1613 Metasync_Connect_Manager::instance()->ensure_plugin_auth_token_exists();
1614 }
1615
1616 public function generate_searchatlas_connect_url()
1617 {
1618 Metasync_Connect_Manager::instance()->generate_searchatlas_connect_url();
1619 }
1620
1621 public function check_searchatlas_connect_status()
1622 {
1623 Metasync_Connect_Manager::instance()->check_searchatlas_connect_status();
1624 }
1625
1626 private function create_searchatlas_nonce_token()
1627 {
1628 return Metasync_Connect_Manager::instance()->create_searchatlas_nonce_token();
1629 }
1630
1631 private function get_client_ip()
1632 {
1633 return Metasync_Connect_Manager::instance()->get_client_ip();
1634 }
1635
1636 private function create_encrypted_searchatlas_token($metadata = array())
1637 {
1638 return Metasync_Connect_Manager::instance()->create_encrypted_searchatlas_token($metadata);
1639 }
1640
1641 private function wp_encrypt_token($payload)
1642 {
1643 return Metasync_Connect_Manager::instance()->wp_encrypt_token($payload);
1644 }
1645
1646 public function test_enhanced_searchatlas_tokens()
1647 {
1648 return Metasync_Connect_Manager::instance()->test_enhanced_searchatlas_tokens();
1649 }
1650
1651 public function test_searchatlas_ajax_endpoint()
1652 {
1653 Metasync_Connect_Manager::instance()->test_searchatlas_ajax_endpoint();
1654 }
1655
1656 public function simple_ajax_test()
1657 {
1658 Metasync_Connect_Manager::instance()->simple_ajax_test();
1659 }
1660
1661 /**
1662 * Create Admin Dashboard Iframe Page
1663 * Embeds the Search Atlas dashboard directly in WordPress admin
1664 */
1665 public function create_admin_dashboard_iframe()
1666 {
1667 Metasync_Admin_Pages::get_instance($this)->create_admin_dashboard_iframe();
1668 }
1669
1670 /**
1671 * Render OTTO cache management interface
1672 */
1673 public function render_otto_cache_management()
1674 {
1675 // Check if OTTO transient cache class exists
1676 if (!class_exists('Metasync_Otto_Transient_Cache')) {
1677 echo '<div class="notice notice-error inline"><p>';
1678 echo '❌ <strong>Error:</strong> Transient Cache class not found.';
1679 echo '</p></div>';
1680 return;
1681 }
1682
1683 // Get cache count
1684 $cache_count = Metasync_Otto_Transient_Cache::get_cache_count();
1685
1686 ?>
1687 <!-- OTTO Cache TTL Setting -->
1688 <div style="margin-bottom: 30px; padding-top: 20px;">
1689 <h4 style="margin-top: 0; color: var(--dashboard-text-primary);"><?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> Cache TTL</h4>
1690 <p style="margin-bottom: 15px; color: var(--dashboard-text-secondary);">
1691 Configure how long <?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> API suggestions are cached before a fresh API call. The stale fallback expires after 12 hours.
1692 </p>
1693
1694 <div style="background: rgba(255,255,255,0.02); border: 1px solid var(--dashboard-border); border-radius: 8px; padding: 20px; margin-bottom: 20px;">
1695 <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 12px;">
1696 <label for="metasync-otto-cache-ttl" style="color: var(--dashboard-text-primary); font-weight: 500;">
1697 Cache TTL (minutes):
1698 </label>
1699 <?php Metasync::render_tooltip_icon('otto_cache_ttl', 'How many minutes OTTO reuses its last SEO suggestions before fetching fresh ones. 30 is fine for most sites.'); ?>
1700 <input type="number"
1701 id="metasync-otto-cache-ttl"
1702 value="<?php echo esc_attr($this->get_otto_cache_ttl_minutes()); ?>"
1703 min="30"
1704 max="1440"
1705 step="1"
1706 style="width: 100px; padding: 8px; border: 1px solid var(--dashboard-border); border-radius: 6px; background: var(--dashboard-card-bg, rgba(255,255,255,0.05)); color: var(--dashboard-text-primary);" />
1707 <span style="color: var(--dashboard-text-secondary); font-size: 13px;">min 30 · max 1440</span>
1708 </div>
1709
1710 <div style="display: flex; align-items: center; gap: 12px;">
1711 <button type="button"
1712 id="metasync-otto-ttl-save-btn"
1713 class="metasync-btn-primary"
1714 style="background: var(--dashboard-gradient-primary); color: #ffffff; border: none; padding: 9px 18px; border-radius: 8px; font-weight: 500; cursor: pointer; transition: all 0.3s ease;"
1715 onmouseover="this.style.transform='translateY(-1px)';"
1716 onmouseout="this.style.transform='translateY(0)';">
1717 Save TTL
1718 </button>
1719 <span id="metasync-otto-ttl-save-msg" style="display: none; font-size: 13px;"></span>
1720 </div>
1721
1722 <input type="hidden" id="metasync-otto-ttl-nonce" value="<?php echo esc_attr(wp_create_nonce('metasync_otto_cache_ttl_nonce')); ?>" />
1723 </div>
1724 </div>
1725
1726 <!-- Cache Plugin Management -->
1727 <div style="margin-bottom: 30px;">
1728 <h4 style="margin-top: 0; color: var(--dashboard-text-primary);">Clear All Cache Plugins</h4>
1729 <p style="margin-bottom: 15px; color: var(--dashboard-text-secondary);">Clear all cache plugins to ensure changes are visible immediately.</p>
1730
1731 <?php
1732 // Display active cache plugins
1733 if (class_exists('Metasync_Cache_Purge')) {
1734 try {
1735 $cache_purge = Metasync_Cache_Purge::get_instance();
1736 $active_cache_plugins = $cache_purge->get_active_cache_plugins();
1737
1738 if (!empty($active_cache_plugins)) {
1739 echo '<p style="color: var(--dashboard-text-primary);"><strong>Active Cache Plugins Detected:</strong></p>';
1740 echo '<ul style="margin-bottom: 15px; color: var(--dashboard-text-primary);">';
1741 foreach ($active_cache_plugins as $plugin_name) {
1742 echo '<li>�
1743 ' . esc_html($plugin_name) . '</li>';
1744 }
1745 echo '</ul>';
1746 } else {
1747 echo '<p style="color: var(--dashboard-text-secondary);">ℹ️ No cache plugins detected.</p>';
1748 }
1749 } catch (Exception $e) {
1750 error_log('MetaSync Cache Status Error: ' . $e->getMessage());
1751 echo '<p style="color: var(--dashboard-error);">⚠️ An error occurred while retrieving cache plugin status.</p>';
1752 }
1753 } else {
1754 echo '<p style="color: var(--dashboard-error);">⚠️ Cache Purge class not loaded.</p>';
1755 }
1756 ?>
1757
1758 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="margin-top: 15px;">
1759 <input type="hidden" name="action" value="metasync_clear_all_cache_plugins" />
1760 <?php wp_nonce_field('metasync_clear_cache_nonce', 'clear_cache_nonce'); ?>
1761 <button type="submit" class="metasync-btn-primary" style="background: var(--dashboard-gradient-primary); color: #ffffff; border: none; padding: 10px 20px; border-radius: 8px; font-weight: 500; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); display: inline-block; width: auto; min-width: 240px; max-width: fit-content;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 8px rgba(0, 0, 0, 0.15)';" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 4px rgba(0, 0, 0, 0.1)';">
1762 <span class="dashicons dashicons-controls-repeat"></span> Clear All Cache Plugins
1763 </button>
1764 <p class="description" style="margin-top: 10px; color: var(--dashboard-text-secondary);">This will clear cache from WP Rocket, LiteSpeed, W3 Total Cache, and all other detected cache plugins.</p>
1765 </form>
1766
1767 <?php
1768 // Display success/error messages
1769 if (isset($_GET['cache_cleared']) && $_GET['cache_cleared'] == '1') {
1770 $cleared = isset($_GET['cleared']) ? intval($_GET['cleared']) : 0;
1771 $failed = isset($_GET['failed']) ? intval($_GET['failed']) : 0;
1772 $plugins = isset($_GET['plugins']) ? sanitize_text_field(wp_unslash($_GET['plugins'])) : '';
1773
1774 if ($cleared > 0) {
1775 echo '<div class="notice notice-success inline" style="margin-top: 15px;"><p>';
1776 echo '�
1777 <strong>Success!</strong> Cleared cache for ' . intval( $cleared ) . ' plugin(s)';
1778 if ($plugins) {
1779 echo ': ' . esc_html($plugins);
1780 }
1781 echo '</p></div>';
1782 } else {
1783 echo '<div class="notice notice-info inline" style="margin-top: 15px;"><p>';
1784 echo 'ℹ️ No cache plugins found to clear. WordPress object cache was cleared.';
1785 echo '</p></div>';
1786 }
1787
1788 if ($failed > 0) {
1789 echo '<div class="notice notice-warning inline" style="margin-top: 15px;"><p>';
1790 echo '⚠️ Failed to clear ' . intval( $failed ) . ' plugin(s).';
1791 echo '</p></div>';
1792 }
1793 }
1794
1795 if (isset($_GET['cache_error']) && $_GET['cache_error'] == '1') {
1796 $message = isset($_GET['message']) ? urldecode(sanitize_text_field(wp_unslash($_GET['message']))) : '';
1797 if (empty($message)) {
1798 $message = 'An unknown error occurred while clearing cache. Please check error logs for details.';
1799 }
1800 echo '<div class="notice notice-error inline" style="margin-top: 15px;"><p>';
1801 echo ' <strong>Error clearing cache:</strong> ' . esc_html($message);
1802 echo '</p></div>';
1803 }
1804 ?>
1805 </div>
1806
1807 <!-- Hosting Cache Integration -->
1808 <?php
1809 $hosting_settings = $this->get_hosting_cache_settings();
1810 $wpe_detected = class_exists('WpeCommon');
1811 $kinsta_detected = class_exists('KinstaCache');
1812 ?>
1813 <div style="margin-bottom: 30px;">
1814 <h4 style="margin-top: 0; color: var(--dashboard-text-primary);">Hosting Cache Integration</h4>
1815 <p style="margin-bottom: 15px; color: var(--dashboard-text-secondary);">
1816 Use your hosting provider's native API to purge the <strong>entire site cache</strong> in one click.
1817 These options are independent of cache plugins and target the server-level cache layer.
1818 </p>
1819
1820 <!-- Detection status badges -->
1821 <div style="display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 20px;">
1822 <span style="display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border-radius: 6px; font-size: 13px; font-weight: 500;
1823 background: <?php echo $wpe_detected ? 'rgba(34,197,94,0.12)' : 'rgba(156,163,175,0.12)'; ?>;
1824 color: <?php echo $wpe_detected ? '#22c55e' : 'var(--dashboard-text-secondary)'; ?>;
1825 border: 1px solid <?php echo $wpe_detected ? 'rgba(34,197,94,0.3)' : 'rgba(156,163,175,0.3)'; ?>;">
1826 <?php echo $wpe_detected ? '�
1827 ' : ''; ?> WP Engine <?php echo $wpe_detected ? '(detected)' : '(not detected)'; ?>
1828 </span>
1829 <span style="display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border-radius: 6px; font-size: 13px; font-weight: 500;
1830 background: <?php echo $kinsta_detected ? 'rgba(34,197,94,0.12)' : 'rgba(156,163,175,0.12)'; ?>;
1831 color: <?php echo $kinsta_detected ? '#22c55e' : 'var(--dashboard-text-secondary)'; ?>;
1832 border: 1px solid <?php echo $kinsta_detected ? 'rgba(34,197,94,0.3)' : 'rgba(156,163,175,0.3)'; ?>;">
1833 <?php echo $kinsta_detected ? '�
1834 ' : '⬜'; ?> Kinsta <?php echo $kinsta_detected ? '(detected)' : '(not detected)'; ?>
1835 </span>
1836 </div>
1837
1838 <!-- Settings toggles -->
1839 <div style="background: rgba(255,255,255,0.02); border: 1px solid var(--dashboard-border); border-radius: 8px; padding: 20px; margin-bottom: 20px;">
1840 <h5 style="margin: 0 0 14px 0; color: var(--dashboard-text-primary);">Enable Native Cache Purge</h5>
1841
1842 <label style="display: flex; align-items: center; gap: 10px; margin-bottom: 12px; cursor: pointer;">
1843 <input type="checkbox"
1844 id="metasync-hc-wpengine"
1845 <?php checked(true, !empty($hosting_settings['wpengine_enabled'])); ?>
1846 <?php echo !$wpe_detected ? 'disabled' : ''; ?>
1847 style="width: 16px; height: 16px; cursor: <?php echo $wpe_detected ? 'pointer' : 'not-allowed'; ?>;" />
1848 <span style="color: var(--dashboard-text-primary); font-weight: 500;">WP Engine</span>
1849 <?php Metasync::render_tooltip_icon('hosting_cache_wpengine', 'If hosted on WP Engine, turn on so OTTO clears the host\'s built-in cache when it updates a page.'); ?>
1850 <span style="color: var(--dashboard-text-secondary); font-size: 12px;">— purges Varnish + Memcached</span>
1851 </label>
1852
1853 <label style="display: flex; align-items: center; gap: 10px; margin-bottom: 16px; cursor: pointer;">
1854 <input type="checkbox"
1855 id="metasync-hc-kinsta"
1856 <?php checked(true, !empty($hosting_settings['kinsta_enabled'])); ?>
1857 <?php echo !$kinsta_detected ? 'disabled' : ''; ?>
1858 style="width: 16px; height: 16px; cursor: <?php echo $kinsta_detected ? 'pointer' : 'not-allowed'; ?>;" />
1859 <span style="color: var(--dashboard-text-primary); font-weight: 500;">Kinsta</span>
1860 <?php Metasync::render_tooltip_icon('hosting_cache_kinsta', 'If hosted on Kinsta, turn on so OTTO clears Kinsta\'s server cache automatically.'); ?>
1861 <span style="color: var(--dashboard-text-secondary); font-size: 12px;">— purges full-page cache (kinsta_cache_purge_full)</span>
1862 </label>
1863
1864 <div style="display: flex; align-items: center; gap: 12px;">
1865 <button type="button"
1866 id="metasync-hc-save-btn"
1867 class="metasync-btn-primary"
1868 style="background: var(--dashboard-gradient-primary); color: #ffffff; border: none; padding: 9px 18px; border-radius: 8px; font-weight: 500; cursor: pointer; transition: all 0.3s ease;"
1869 onmouseover="this.style.transform='translateY(-1px)';"
1870 onmouseout="this.style.transform='translateY(0)';">
1871 Save Settings
1872 </button>
1873 <span id="metasync-hc-save-msg" style="display: none; font-size: 13px;"></span>
1874 </div>
1875
1876 <input type="hidden" id="metasync-hc-nonce" value="<?php echo esc_attr(wp_create_nonce('metasync_hosting_cache_nonce')); ?>" />
1877 </div>
1878
1879 <!-- Purge button -->
1880 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
1881 <input type="hidden" name="action" value="metasync_purge_hosting_cache" />
1882 <?php wp_nonce_field('metasync_hosting_cache_purge_nonce', 'hosting_cache_purge_nonce'); ?>
1883 <button type="submit"
1884 class="metasync-btn-primary"
1885 style="background: var(--dashboard-gradient-primary); color: #ffffff; border: none; padding: 10px 20px; border-radius: 8px; font-weight: 500; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 2px 4px rgba(0,0,0,0.1); display: inline-block; min-width: 240px; max-width: fit-content;"
1886 onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 8px rgba(0,0,0,0.15)';"
1887 onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 4px rgba(0,0,0,0.1)';">
1888 <span class="dashicons dashicons-update" style="margin-top:3px;font-size:15px;width:15px;height:15px;"></span> Purge Entire Hosting Cache
1889 </button>
1890 <p class="description" style="margin-top: 10px; color: var(--dashboard-text-secondary);">
1891 Triggers a full-site cache purge using the native WP Engine and/or Kinsta APIs (based on toggles above).
1892 </p>
1893 </form>
1894
1895 <?php
1896 // Hosting cache result messages
1897 if (isset($_GET['hosting_cache_cleared']) && $_GET['hosting_cache_cleared'] == '1') {
1898 $hc_cleared = isset($_GET['hc_cleared']) ? sanitize_text_field(urldecode($_GET['hc_cleared'])) : '';
1899 $hc_failed = isset($_GET['hc_failed']) ? sanitize_text_field(urldecode($_GET['hc_failed'])) : '';
1900 $hc_not_detected = isset($_GET['hc_not_detected']) ? sanitize_text_field(urldecode($_GET['hc_not_detected'])) : '';
1901
1902 if ($hc_cleared) {
1903 echo '<div class="notice notice-success inline" style="margin-top: 15px;"><p>';
1904 echo '�
1905 <strong>Success!</strong> Purged hosting cache on: ' . esc_html($hc_cleared);
1906 echo '</p></div>';
1907 }
1908 if ($hc_failed) {
1909 echo '<div class="notice notice-error inline" style="margin-top: 10px;"><p>';
1910 echo ' <strong>Failed</strong> to purge: ' . esc_html($hc_failed);
1911 echo '</p></div>';
1912 }
1913 if ($hc_not_detected && !$hc_cleared && !$hc_failed) {
1914 echo '<div class="notice notice-info inline" style="margin-top: 10px;"><p>';
1915 echo 'ℹ️ No enabled hosting providers were detected on this server (' . esc_html($hc_not_detected) . ').';
1916 echo '</p></div>';
1917 }
1918 }
1919 ?>
1920
1921 <script>
1922 jQuery(document).ready(function($) {
1923 $('#metasync-hc-save-btn').on('click', function() {
1924 var $btn = $(this);
1925 var $msg = $('#metasync-hc-save-msg');
1926
1927 $btn.prop('disabled', true).text('Saving…');
1928
1929 $.ajax({
1930 url: ajaxurl,
1931 type: 'POST',
1932 data: {
1933 action: 'metasync_save_hosting_cache_settings',
1934 hosting_cache_nonce: $('#metasync-hc-nonce').val(),
1935 wpengine_enabled: $('#metasync-hc-wpengine').is(':checked') ? '1' : '0',
1936 kinsta_enabled: $('#metasync-hc-kinsta').is(':checked') ? '1' : '0',
1937 },
1938 success: function(response) {
1939 if (response.success) {
1940 $msg.text('�
1941 Saved').css('color', '#22c55e').show();
1942 } else {
1943 $msg.text('' + (response.data.message || 'Save failed')).css('color', '#ef4444').show();
1944 }
1945 },
1946 error: function() {
1947 $msg.text('❌ Request failed').css('color', '#ef4444').show();
1948 },
1949 complete: function() {
1950 $btn.prop('disabled', false).text('💾 Save Settings');
1951 setTimeout(function() { $msg.fadeOut(); }, 4000);
1952 }
1953 });
1954 });
1955
1956 // OTTO Cache TTL save
1957 $('#metasync-otto-ttl-save-btn').on('click', function() {
1958 var $btn = $(this);
1959 var $msg = $('#metasync-otto-ttl-save-msg');
1960 var ttl = parseInt($('#metasync-otto-cache-ttl').val(), 10);
1961
1962 if (isNaN(ttl) || ttl < 30 || ttl > 1440) {
1963 $msg.text('❌ TTL must be between 30 and 1440 minutes.').css('color', '#ef4444').show();
1964 return;
1965 }
1966
1967 $btn.prop('disabled', true).text('Saving…');
1968
1969 $.ajax({
1970 url: ajaxurl,
1971 type: 'POST',
1972 data: {
1973 action: 'metasync_save_otto_cache_ttl',
1974 otto_cache_ttl_nonce: $('#metasync-otto-ttl-nonce').val(),
1975 otto_cache_ttl: ttl,
1976 },
1977 success: function(response) {
1978 if (response.success) {
1979 $msg.text('�
1980 Saved').css('color', '#22c55e').show();
1981 } else {
1982 $msg.text('' + (response.data && response.data.message ? response.data.message : 'Save failed')).css('color', '#ef4444').show();
1983 }
1984 },
1985 error: function() {
1986 $msg.text('❌ Request failed').css('color', '#ef4444').show();
1987 },
1988 complete: function() {
1989 $btn.prop('disabled', false).text('Save TTL');
1990 setTimeout(function() { $msg.fadeOut(); }, 4000);
1991 }
1992 });
1993 });
1994 });
1995 </script>
1996 </div>
1997
1998 <!-- Object Cache Behaviour -->
1999 <?php $targeted_cache_enabled = get_option('metasync_targeted_object_cache', '1'); ?>
2000 <div style="margin-bottom: 30px;">
2001 <h4 style="margin-top: 0; color: var(--dashboard-text-primary);">Object Cache Behaviour</h4>
2002 <p style="margin-bottom: 15px; color: var(--dashboard-text-secondary);">
2003 Controls how the WordPress object cache (Redis/Memcached) is cleared when OTTO updates pages.
2004 </p>
2005
2006 <div style="background: rgba(255,255,255,0.02); border: 1px solid var(--dashboard-border); border-radius: 8px; padding: 20px; margin-bottom: 20px;">
2007 <label style="display: flex; align-items: flex-start; gap: 10px; cursor: pointer;">
2008 <input type="checkbox"
2009 id="metasync-targeted-object-cache"
2010 <?php checked('1', $targeted_cache_enabled); ?>
2011 style="width: 16px; height: 16px; cursor: pointer; margin-top: 2px; flex-shrink: 0;" />
2012 <span>
2013 <span style="color: var(--dashboard-text-primary); font-weight: 500; display: block; margin-bottom: 4px;">Targeted Object Cache Purge<?php Metasync::render_tooltip_icon('targeted_object_cache_purge', 'Leave this on. Refreshes only the pages OTTO changed instead of wiping your whole site\'s memory cache — safer/faster on large sites.'); ?></span>
2014 <span style="color: var(--dashboard-text-secondary); font-size: 12px;">
2015 When enabled, only the updated posts are evicted from the object cache (recommended for large sites).
2016 When disabled, a full <code>wp_cache_flush()</code> is used instead.
2017 </span>
2018 </span>
2019 </label>
2020
2021 <div style="display: flex; align-items: center; gap: 12px; margin-top: 16px;">
2022 <button type="button"
2023 id="metasync-toc-save-btn"
2024 class="metasync-btn-primary"
2025 style="background: var(--dashboard-gradient-primary); color: #ffffff; border: none; padding: 9px 18px; border-radius: 8px; font-weight: 500; cursor: pointer; transition: all 0.3s ease;"
2026 onmouseover="this.style.transform='translateY(-1px)';"
2027 onmouseout="this.style.transform='translateY(0)';">
2028 Save Settings
2029 </button>
2030 <span id="metasync-toc-save-msg" style="display: none; font-size: 13px;"></span>
2031 </div>
2032
2033 <input type="hidden" id="metasync-toc-nonce" value="<?php echo esc_attr(wp_create_nonce('metasync_object_cache_nonce')); ?>" />
2034 </div>
2035
2036 <script>
2037 jQuery(document).ready(function($) {
2038 $('#metasync-toc-save-btn').on('click', function() {
2039 var $btn = $(this);
2040 var $msg = $('#metasync-toc-save-msg');
2041
2042 $btn.prop('disabled', true).text('Saving…');
2043
2044 $.ajax({
2045 url: ajaxurl,
2046 type: 'POST',
2047 data: {
2048 action: 'metasync_save_object_cache_settings',
2049 object_cache_nonce: $('#metasync-toc-nonce').val(),
2050 targeted_object_cache: $('#metasync-targeted-object-cache').is(':checked') ? '1' : '0',
2051 },
2052 success: function(response) {
2053 if (response.success) {
2054 $msg.text('�
2055 Saved').css('color', '#22c55e').show();
2056 } else {
2057 $msg.text('' + (response.data.message || 'Save failed')).css('color', '#ef4444').show();
2058 }
2059 },
2060 error: function() {
2061 $msg.text('❌ Request failed').css('color', '#ef4444').show();
2062 },
2063 complete: function() {
2064 $btn.prop('disabled', false).text('💾 Save Settings');
2065 setTimeout(function() { $msg.fadeOut(); }, 4000);
2066 }
2067 });
2068 });
2069 });
2070 </script>
2071 </div>
2072
2073 <!-- OTTO Transient Cache -->
2074 <div style="margin-bottom: 30px;">
2075 <h4 style="margin-top: 0; color: var(--dashboard-text-primary);"><?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> Transient Cache</h4>
2076 <p style="margin-bottom: 15px; color: var(--dashboard-text-secondary);">
2077 Manage <?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> suggestions cache. Clearing cache will force fresh API calls on next page load.
2078 </p>
2079
2080 <div style="background: rgba(255, 255, 255, 0.05); padding: 12px; border-radius: 4px; margin-bottom: 20px; border: 1px solid var(--dashboard-border);">
2081 <strong style="color: var(--dashboard-text-primary);">Current Cache Status:</strong>
2082 <span style="color: var(--dashboard-accent);"><?php echo esc_html($cache_count); ?> cached entries</span>
2083 </div>
2084
2085 <?php
2086 // Display success/error messages
2087 if (isset($_GET['otto_cache_cleared']) && $_GET['otto_cache_cleared'] == '1') {
2088 $cleared_count = isset($_GET['count']) ? intval($_GET['count']) : 0;
2089 $url = isset($_GET['url']) ? urldecode(sanitize_text_field(wp_unslash($_GET['url']))) : '';
2090
2091 echo '<div class="notice notice-success inline" style="margin-top: 15px;"><p>';
2092 if (!empty($url)) {
2093 echo '�
2094 <strong>Success!</strong> Cleared cache for URL: <code>' . esc_html($url) . '</code> (' . intval( $cleared_count ) . ' entries)';
2095 } else {
2096 echo '�
2097 <strong>Success!</strong> Cleared entire transient cache (' . intval( $cleared_count ) . ' entries)';
2098 }
2099 echo '</p></div>';
2100 }
2101
2102 if (isset($_GET['otto_cache_error']) && $_GET['otto_cache_error'] == '1') {
2103 $message = isset($_GET['message']) ? urldecode(sanitize_text_field(wp_unslash($_GET['message']))) : 'An unknown error occurred.';
2104 echo '<div class="notice notice-error inline" style="margin-top: 15px;"><p>';
2105 echo '❌ <strong>Error:</strong> ' . esc_html($message);
2106 echo '</p></div>';
2107 }
2108 ?>
2109
2110 <!-- Clear Entire Cache -->
2111 <div style="margin-bottom: 30px; padding: 20px; border: 1px solid var(--dashboard-border); border-radius: 4px; background: rgba(255, 255, 255, 0.02);">
2112 <h5 style="margin-top: 0; color: var(--dashboard-text-primary);">Clear Entire Transient Cache</h5>
2113 <p style="color: var(--dashboard-text-secondary); margin-bottom: 15px;">
2114 This will clear all <?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> transient cache entries (suggestions, locks, stale cache, rate limits).
2115 </p>
2116 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"
2117 onsubmit="return confirm('Are you sure you want to clear the entire transient cache? This will force fresh API calls for all URLs.');">
2118 <input type="hidden" name="action" value="metasync_clear_otto_cache_all" />
2119 <?php wp_nonce_field('metasync_clear_otto_cache_nonce', 'clear_otto_cache_nonce'); ?>
2120 <button type="submit" class="metasync-btn-primary" style="background: var(--dashboard-gradient-primary); color: #ffffff; border: none; padding: 10px 20px; border-radius: 8px; font-weight: 500; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); display: inline-block; width: auto; min-width: 240px; max-width: fit-content;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 8px rgba(0, 0, 0, 0.15)';" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 4px rgba(0, 0, 0, 0.1)';">
2121 <span class="dashicons dashicons-trash" style="margin-top:3px;font-size:15px;width:15px;height:15px;"></span> Clear Entire Cache
2122 </button>
2123 </form>
2124 </div>
2125
2126 <!-- Clear Cache by URL -->
2127 <div style="padding: 20px; border: 1px solid var(--dashboard-border); border-radius: 4px; background: rgba(255, 255, 255, 0.02);">
2128 <h5 style="margin-top: 0; color: var(--dashboard-text-primary);">Clear Cache by URL</h5>
2129 <p style="color: var(--dashboard-text-secondary); margin-bottom: 15px;">
2130 Enter a specific URL to clear its cached <?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> suggestions. Use the full URL including protocol (e.g., https://example.com/page/).
2131 </p>
2132 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
2133 <input type="hidden" name="action" value="metasync_clear_otto_cache_url" />
2134 <?php wp_nonce_field('metasync_clear_otto_cache_nonce', 'clear_otto_cache_nonce'); ?>
2135 <table class="form-table">
2136 <tr>
2137 <th scope="row">
2138 <label for="otto_cache_url" style="color: var(--dashboard-text-primary);">URL to Clear</label>
2139 </th>
2140 <td>
2141 <input type="url"
2142 id="otto_cache_url"
2143 name="otto_cache_url"
2144 value="<?php echo isset($_GET['url']) ? esc_attr(urldecode(sanitize_text_field(wp_unslash($_GET['url'])))) : ''; ?>"
2145 class="regular-text"
2146 placeholder="https://example.com/page/"
2147 required />
2148 <p class="description" style="color: var(--dashboard-text-secondary);">Enter the full URL of the page whose cache you want to clear.</p>
2149 </td>
2150 </tr>
2151 </table>
2152 <button type="submit" class="metasync-btn-primary" style="background: var(--dashboard-gradient-primary); color: #ffffff; border: none; padding: 10px 20px; border-radius: 8px; font-weight: 500; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); display: inline-block; width: auto; max-width: fit-content;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 8px rgba(0, 0, 0, 0.15)';" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 4px rgba(0, 0, 0, 0.1)';">
2153 <span class="dashicons dashicons-trash" style="margin-top:3px;font-size:15px;width:15px;height:15px;"></span> Clear Cache for URL
2154 </button>
2155 </form>
2156 </div>
2157 </div>
2158 <?php
2159 }
2160
2161 /**
2162 * Render debug mode section for inclusion in Advanced settings
2163 */
2164 public function render_debug_mode_section()
2165 {
2166 Metasync_Debug_Manager::instance()->render_debug_mode_section();
2167 }
2168
2169 /**
2170 * Render error log content for inclusion in Advanced settings
2171 */
2172 public function render_error_log_content()
2173 {
2174 Metasync_Debug_Manager::instance()->render_error_log_content();
2175 }
2176
2177 /**
2178 * WordPress standard handler for clearing all cache plugins (admin_post hook)
2179 * This method runs early and prevents any output before redirect
2180 */
2181 public function handle_clear_all_cache_plugins() {
2182 Metasync_Otto_Cache_Manager::instance()->handle_clear_all_cache_plugins();
2183 }
2184
2185 /**
2186 * WordPress standard handler for clearing OTTO cache (admin_post hook)
2187 */
2188 public function handle_clear_otto_cache_all() {
2189 Metasync_Otto_Cache_Manager::instance()->handle_clear_otto_cache_all();
2190 }
2191
2192 /**
2193 * WordPress standard handler for clearing OTTO cache by URL (admin_post hook)
2194 */
2195 public function handle_clear_otto_cache_url() {
2196 Metasync_Otto_Cache_Manager::instance()->handle_clear_otto_cache_url();
2197 }
2198
2199 /**
2200 * Get hosting cache integration settings with defaults
2201 *
2202 * @return array Settings array with 'wpengine_enabled' and 'kinsta_enabled' keys
2203 */
2204 private function get_hosting_cache_settings() {
2205 $defaults = array(
2206 'wpengine_enabled' => true,
2207 'kinsta_enabled' => true,
2208 );
2209 $saved = get_option('metasync_hosting_cache_options', array());
2210 return wp_parse_args($saved, $defaults);
2211 }
2212
2213 /**
2214 * AJAX handler for saving hosting cache settings
2215 */
2216 public function ajax_save_hosting_cache_settings() {
2217 if (!isset($_POST['hosting_cache_nonce']) || !wp_verify_nonce($_POST['hosting_cache_nonce'], 'metasync_hosting_cache_nonce')) {
2218 wp_send_json_error(array('message' => 'Security check failed'));
2219 return;
2220 }
2221
2222 if (!Metasync::current_user_has_plugin_access()) {
2223 wp_send_json_error(array('message' => 'Insufficient permissions'));
2224 return;
2225 }
2226
2227 $settings = array(
2228 'wpengine_enabled' => !empty($_POST['wpengine_enabled']) && $_POST['wpengine_enabled'] === '1',
2229 'kinsta_enabled' => !empty($_POST['kinsta_enabled']) && $_POST['kinsta_enabled'] === '1',
2230 );
2231
2232 update_option('metasync_hosting_cache_options', $settings);
2233 wp_send_json_success(array('message' => 'Hosting cache settings saved'));
2234 }
2235
2236 /**
2237 * AJAX handler: save object cache behaviour settings
2238 */
2239 public function ajax_save_object_cache_settings() {
2240 if (!isset($_POST['object_cache_nonce']) || !wp_verify_nonce($_POST['object_cache_nonce'], 'metasync_object_cache_nonce')) {
2241 wp_send_json_error(array('message' => 'Security check failed'));
2242 return;
2243 }
2244
2245 if (!Metasync::current_user_has_plugin_access()) {
2246 wp_send_json_error(array('message' => 'Insufficient permissions'));
2247 return;
2248 }
2249
2250 $targeted = (!empty($_POST['targeted_object_cache']) && $_POST['targeted_object_cache'] === '1') ? '1' : '0';
2251 update_option('metasync_targeted_object_cache', $targeted);
2252 wp_send_json_success(array('message' => 'Object cache settings saved'));
2253 }
2254
2255 /**
2256 * Get OTTO Cache TTL value in minutes from execution settings.
2257 *
2258 * @return int
2259 */
2260 private function get_otto_cache_ttl_minutes() {
2261 $execution_settings = get_option('metasync_execution_settings', array());
2262 return isset($execution_settings['otto_cache_ttl']) ? absint($execution_settings['otto_cache_ttl']) : 30;
2263 }
2264
2265 /**
2266 * AJAX handler for saving OTTO Cache TTL
2267 */
2268 public function ajax_save_otto_cache_ttl() {
2269 if (!isset($_POST['otto_cache_ttl_nonce']) || !wp_verify_nonce($_POST['otto_cache_ttl_nonce'], 'metasync_otto_cache_ttl_nonce')) {
2270 wp_send_json_error(array('message' => 'Security check failed.'));
2271 return;
2272 }
2273
2274 if (!current_user_can('manage_options')) {
2275 wp_send_json_error(array('message' => 'Insufficient permissions.'));
2276 return;
2277 }
2278
2279 $ttl = isset($_POST['otto_cache_ttl']) ? absint($_POST['otto_cache_ttl']) : 30;
2280
2281 if ($ttl < 30 || $ttl > 1440) {
2282 wp_send_json_error(array('message' => sprintf('%s Cache TTL must be between 30 and 1440 minutes.', Metasync::get_whitelabel_otto_name())));
2283 return;
2284 }
2285
2286 $settings = get_option('metasync_execution_settings', array());
2287 $settings['otto_cache_ttl'] = $ttl;
2288 update_option('metasync_execution_settings', $settings);
2289
2290 wp_send_json_success(array('message' => sprintf('%s Cache TTL saved.', Metasync::get_whitelabel_otto_name())));
2291 }
2292
2293 /**
2294 * admin_post handler: purge WP Engine and Kinsta hosting-level caches
2295 */
2296 public function handle_purge_hosting_cache() {
2297 if (!isset($_POST['hosting_cache_purge_nonce']) || !wp_verify_nonce($_POST['hosting_cache_purge_nonce'], 'metasync_hosting_cache_purge_nonce')) {
2298 wp_die('Security check failed');
2299 }
2300
2301 if (!Metasync::current_user_has_plugin_access()) {
2302 wp_die('You do not have permission to perform this action');
2303 }
2304
2305 $redirect_url = admin_url('admin.php?page=' . self::$page_slug . '&tab=advanced');
2306 $settings = $this->get_hosting_cache_settings();
2307 $cleared = array();
2308 $failed = array();
2309 $not_detected = array();
2310
2311 // WP Engine native purge (Varnish + Memcached)
2312 if (!empty($settings['wpengine_enabled'])) {
2313 if (class_exists('WpeCommon')) {
2314 try {
2315 WpeCommon::purge_varnish_cache();
2316 WpeCommon::purge_memcached();
2317 $cleared[] = 'WP Engine';
2318 } catch (Exception $e) {
2319 error_log('MetaSync: WP Engine hosting cache purge failed - ' . $e->getMessage());
2320 $failed[] = 'WP Engine';
2321 }
2322 } else {
2323 $not_detected[] = 'WP Engine';
2324 }
2325 }
2326
2327 // Kinsta native purge (full site)
2328 if (!empty($settings['kinsta_enabled'])) {
2329 if (class_exists('KinstaCache')) {
2330 try {
2331 KinstaCache::get_instance()->kinsta_cache_purge_full();
2332 $cleared[] = 'Kinsta';
2333 } catch (Exception $e) {
2334 error_log('MetaSync: Kinsta hosting cache purge failed - ' . $e->getMessage());
2335 $failed[] = 'Kinsta';
2336 }
2337 } else {
2338 $not_detected[] = 'Kinsta';
2339 }
2340 }
2341
2342 $redirect_url .= '&hosting_cache_cleared=1';
2343 if (!empty($cleared)) {
2344 $redirect_url .= '&hc_cleared=' . urlencode(implode(',', $cleared));
2345 }
2346 if (!empty($failed)) {
2347 $redirect_url .= '&hc_failed=' . urlencode(implode(',', $failed));
2348 }
2349 if (!empty($not_detected)) {
2350 $redirect_url .= '&hc_not_detected=' . urlencode(implode(',', $not_detected));
2351 }
2352
2353 wp_safe_redirect($redirect_url);
2354 exit;
2355 }
2356
2357 /**
2358 * Handle debug mode operations (enable/disable/extend)
2359 */
2360 private function handle_debug_mode_operations()
2361 {
2362 Metasync_Debug_Manager::instance()->handle_debug_mode_operations();
2363 }
2364
2365 /**
2366 * Handle error log operations (clear)
2367 */
2368 private function handle_error_log_operations()
2369 {
2370 Metasync_Debug_Manager::instance()->handle_error_log_operations();
2371 }
2372
2373 /**
2374 * Handle clear all settings operations
2375 */
2376 private function handle_clear_all_settings()
2377 {
2378 Metasync_Debug_Manager::instance()->handle_clear_all_settings();
2379 }
2380
2381 /**
2382 * Handle saving plugin access roles from Advanced Settings
2383 * @deprecated Now handled by Metasync_Settings_Registration::settings_page_init()
2384 */
2385 private function handle_plugin_access_roles_save() {
2386 }
2387
2388 /**
2389 * Get error log content for display
2390 */
2391 private function get_error_log_content()
2392 {
2393 return Metasync_Debug_Manager::instance()->get_error_log_content();
2394 }
2395
2396 /**
2397 * Memory-efficient function to get last N lines from a large file
2398 */
2399 private function get_log_tail($file_path, $lines = null)
2400 {
2401 return Metasync_Debug_Manager::instance()->get_log_tail($file_path, $lines);
2402 }
2403
2404 /**
2405 * Test whitelabel domain functionality (development/debugging)
2406 */
2407 public function test_whitelabel_domain()
2408 {
2409 Metasync_Connect_Manager::instance()->test_whitelabel_domain();
2410 }
2411
2412 /**
2413 * Decrypt token using WordPress SALTs
2414 */
2415 private function wp_decrypt_token($encrypted_token)
2416 {
2417 return Metasync_Connect_Manager::instance()->wp_decrypt_token($encrypted_token);
2418 }
2419
2420 /**
2421 * Get active JWT token for the plugin
2422 * Public static method accessible from anywhere in the plugin
2423 *
2424 * @param bool $force_refresh Force generation of new token even if cached one exists
2425 * @return string|false JWT token on success, false on failure
2426 */
2427 public static function get_active_jwt_token($force_refresh = false)
2428 {
2429 return Metasync_Connect_Manager::instance()->get_active_jwt_token($force_refresh);
2430 }
2431
2432
2433 /**
2434 * Get fresh JWT token from Search Atlas API with caching
2435 * Generates and caches JWT tokens to avoid repeated API calls
2436 *
2437 * @return string|false JWT token on success, false on failure
2438 */
2439 public function get_fresh_jwt_token()
2440 {
2441 return Metasync_Connect_Manager::instance()->get_fresh_jwt_token();
2442 }
2443
2444 /**
2445 * Clear cached JWT tokens
2446 * Useful when authentication is reset or API key changes
2447 */
2448 private function clear_jwt_token_cache()
2449 {
2450 Metasync_Connect_Manager::instance()->clear_jwt_token_cache();
2451 }
2452
2453
2454
2455 /**
2456 * Data or Response received from HeartBeat API for admin area.
2457 */
2458 public function lgSendCustomerParams()
2459 {
2460 Metasync_Admin_Ajax::instance()->lgSendCustomerParams();
2461 }
2462
2463
2464
2465 /**
2466 * Add CSS styles for Search Atlas admin bar status indicator
2467 */
2468 public function metasync_admin_bar_style()
2469 {
2470 Metasync_Admin_Navigation::instance()->metasync_admin_bar_style();
2471 }
2472
2473 /**
2474 * Add Search Atlas status indicator to WordPress admin bar
2475 * Shows sync status with green/red emoji
2476 */
2477 public function add_searchatlas_admin_bar_status($wp_admin_bar)
2478 {
2479 Metasync_Admin_Navigation::instance()->add_searchatlas_admin_bar_status($wp_admin_bar);
2480 }
2481
2482 // ------------------------------------------------------------------
2483 // Heartbeat / connection-monitoring – delegated to Metasync_Heartbeat_Manager
2484 // ------------------------------------------------------------------
2485
2486 public function is_heartbeat_connected($general_settings = null)
2487 {
2488 return Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general_settings);
2489 }
2490
2491 public function fetch_public_hash($otto_pixel_uuid, $jwt_token)
2492 {
2493 return Metasync_Heartbeat_Manager::instance()->fetch_public_hash($otto_pixel_uuid, $jwt_token);
2494 }
2495
2496 public function schedule_heartbeat_cron()
2497 {
2498 Metasync_Heartbeat_Manager::instance()->schedule_heartbeat_cron();
2499 }
2500
2501 public function unschedule_heartbeat_cron()
2502 {
2503 Metasync_Heartbeat_Manager::instance()->unschedule_heartbeat_cron();
2504 }
2505
2506 public function execute_heartbeat_cron_check()
2507 {
2508 return Metasync_Heartbeat_Manager::instance()->execute_heartbeat_cron_check();
2509 }
2510
2511 public function add_heartbeat_cron_schedule($schedules)
2512 {
2513 return Metasync_Heartbeat_Manager::instance()->add_heartbeat_cron_schedule($schedules);
2514 }
2515
2516 public function get_heartbeat_state()
2517 {
2518 return Metasync_Heartbeat_Manager::instance()->get_heartbeat_state();
2519 }
2520
2521 public function set_heartbeat_state_key_pending()
2522 {
2523 Metasync_Heartbeat_Manager::instance()->set_heartbeat_state_key_pending();
2524 }
2525
2526 public function execute_burst_heartbeat()
2527 {
2528 Metasync_Heartbeat_Manager::instance()->execute_burst_heartbeat();
2529 }
2530
2531 public function execute_announce_cron()
2532 {
2533 Metasync_Heartbeat_Manager::instance()->execute_announce_cron();
2534 }
2535
2536 public function unschedule_burst_heartbeat_cron()
2537 {
2538 Metasync_Heartbeat_Manager::instance()->unschedule_burst_heartbeat_cron();
2539 }
2540
2541 public function unschedule_announce_cron()
2542 {
2543 Metasync_Heartbeat_Manager::instance()->unschedule_announce_cron();
2544 }
2545
2546 public function maybe_schedule_heartbeat_cron()
2547 {
2548 Metasync_Heartbeat_Manager::instance()->maybe_schedule_heartbeat_cron();
2549 }
2550
2551
2552 public function trigger_immediate_heartbeat_check($context = 'Manual trigger')
2553 {
2554 return Metasync_Heartbeat_Manager::instance()->trigger_immediate_heartbeat_check($context);
2555 }
2556
2557 public function handle_immediate_heartbeat_trigger($context = 'WordPress action trigger')
2558 {
2559 Metasync_Heartbeat_Manager::instance()->handle_immediate_heartbeat_trigger($context);
2560 }
2561
2562 public function ajax_burst_ping()
2563 {
2564 Metasync_Heartbeat_Manager::instance()->ajax_burst_ping();
2565 }
2566
2567 public function update_heartbeat_cache_after_sync($is_connected, $context = 'Sync operation')
2568 {
2569 return Metasync_Heartbeat_Manager::instance()->update_heartbeat_cache_after_sync($is_connected, $context);
2570 }
2571
2572
2573 /**
2574 * Refresh Plugin Auth Token
2575 * Generates a new Plugin Auth Token and updates heartbeat API
2576 */
2577 public function refresh_plugin_auth_token()
2578 {
2579 Metasync_Connect_Manager::instance()->refresh_plugin_auth_token();
2580 }
2581
2582 public function get_plugin_auth_token()
2583 {
2584 Metasync_Connect_Manager::instance()->get_plugin_auth_token();
2585 }
2586
2587 public function reset_searchatlas_authentication()
2588 {
2589 Metasync_Connect_Manager::instance()->reset_searchatlas_authentication();
2590 }
2591
2592 private function cleanup_searchatlas_nonce_tokens()
2593 {
2594 return Metasync_Connect_Manager::instance()->cleanup_searchatlas_nonce_tokens();
2595 }
2596
2597 private function cleanup_searchatlas_rate_limits()
2598 {
2599 return Metasync_Connect_Manager::instance()->cleanup_searchatlas_rate_limits();
2600 }
2601
2602 private function get_available_menu_items()
2603 {
2604 return Metasync_Admin_Navigation::instance()->get_available_menu_items();
2605 }
2606
2607 /**
2608 * Add options page
2609 */
2610 public function add_plugin_settings_page()
2611 {
2612 Metasync_Admin_Navigation::instance()->add_plugin_settings_page($this);
2613 }
2614
2615 /**
2616 * General Options page callback
2617 */
2618 public function create_admin_settings_page()
2619 {
2620 Metasync_Admin_Pages::get_instance($this)->create_admin_settings_page();
2621 }
2622
2623 public function render_navigation_menu($current_page = null)
2624 {
2625 Metasync_Admin_Navigation::instance()->render_navigation_menu($current_page);
2626 }
2627
2628 public function render_plugin_header($page_title = null)
2629 {
2630 Metasync_Admin_Navigation::instance()->render_plugin_header($page_title);
2631 }
2632
2633 /**
2634 * Open the Yoast-style 3-column page layout.
2635 * Must be paired with render_layout_close().
2636 */
2637 public function render_layout_open($page_title = '', $current_page = '', $description = '')
2638 {
2639 Metasync_Admin_Navigation::instance()->render_layout_open($page_title, $current_page, $description);
2640 }
2641
2642 /**
2643 * Close the 3-column layout opened by render_layout_open().
2644 *
2645 * @param bool $show_promo Whether to render the right promo sidebar. Default true.
2646 */
2647 public function render_layout_close($show_promo = true)
2648 {
2649 Metasync_Admin_Navigation::instance()->render_layout_close($show_promo);
2650 }
2651
2652 /*
2653 Method to handle Ajax request from "General Settings" page
2654 */
2655 public function meta_sync_save_settings() {
2656 Metasync_Settings_Registration::instance()->meta_sync_save_settings();
2657 }
2658
2659 /**
2660 * AJAX handler for saving execution settings
2661 */
2662 public function ajax_save_execution_settings() {
2663 Metasync_Settings_Registration::instance()->ajax_save_execution_settings();
2664 }
2665
2666 /**
2667 * Get Action Scheduler concurrent batches from execution settings
2668 *
2669 * @param int $default_batches Default concurrent batches
2670 * @return int Configured concurrent batches
2671 */
2672 public function get_action_scheduler_batches($default_batches) {
2673 // Only apply if Action Scheduler is active
2674 if (!class_exists('ActionScheduler')) {
2675 return $default_batches;
2676 }
2677
2678 return $this->get_execution_setting('action_scheduler_batches');
2679 }
2680
2681 /**
2682 * Get Action Scheduler retention period from execution settings
2683 * Converts days to seconds for Action Scheduler
2684 *
2685 * @param int $default_seconds Default retention period in seconds
2686 * @return int Configured retention period in seconds
2687 */
2688 public function get_action_scheduler_retention_period($default_seconds) {
2689 // Only apply if Action Scheduler is active
2690 if (!class_exists('ActionScheduler')) {
2691 return $default_seconds; // Default is 30 days = 2592000 seconds
2692 }
2693
2694 $cleanup_days = $this->get_execution_setting('queue_cleanup_days');
2695 return $cleanup_days * DAY_IN_SECONDS;
2696 }
2697
2698 /*
2699 * Sync setting on CRUD term category
2700 */
2701
2702 public function admin_crud_term($term_id,$term_tax_id,$taxonomy)
2703 {
2704 # Handle term creation, update, or deletion
2705 $this->sync_term($term_id, $taxonomy);
2706
2707 }
2708
2709
2710 /*
2711 * Sync setting on Delete term category
2712 */
2713
2714 public function admin_delete_term($term_id,$taxonomy)
2715 {
2716 # Handle term deletion
2717 $this->sync_term($term_id, $taxonomy);
2718 }
2719
2720 /*
2721 * Handle category deletion - Sync after category is deleted
2722 * This hook fires AFTER the category is fully deleted from the database
2723 */
2724 public function admin_delete_category($term_id)
2725 {
2726 try {
2727 # Initialize MetaSync API request class and trigger synchronization
2728 (new Metasync_Sync_Requests())->SyncCustomerParams();
2729 } catch (Exception $e) {
2730 # Log any API request errors for debugging
2731 error_log('Metasync API Error: ' . $e->getMessage());
2732 }
2733 }
2734
2735 /*
2736 * Call the SYNC API
2737 */
2738 private function sync_term($term_id, $taxonomy)
2739 {
2740 # Ensure the term belongs to the 'category' taxonomy and is not an error
2741 if ($taxonomy !== 'category' ) return;
2742
2743 try {
2744 # Initialize MetaSync API request class and trigger synchronization
2745 (new Metasync_Sync_Requests())->SyncCustomerParams();
2746 } catch (Exception $e) {
2747 # Log any API request errors for debugging
2748 error_log('Metasync API Error: ' . $e->getMessage());
2749 }
2750 }
2751
2752 /**
2753 * Dashboard page callback
2754 */
2755 public function create_admin_dashboard_page()
2756 {
2757 Metasync_Admin_Pages::get_instance($this)->create_admin_dashboard_page();
2758 }
2759
2760 /**
2761 * Robots.txt page callback
2762 */
2763 public function create_admin_robots_txt_page()
2764 {
2765 Metasync_Admin_Pages::get_instance($this)->create_admin_robots_txt_page();
2766 }
2767
2768 /**
2769 * Media Optimization page callback
2770 */
2771 public function create_admin_media_optimization_page()
2772 {
2773 $this->render_layout_open('Media Optimization', 'media_optimization', 'Compress and optimize images to improve page load speed.');
2774 // Load media optimization settings class
2775 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-settings.php';
2776
2777 $save_success = false;
2778
2779 // Handle form submissions
2780 if (isset($_POST['metasync_media_optimization_nonce'])) {
2781 check_admin_referer('metasync_save_media_optimization', 'metasync_media_optimization_nonce');
2782
2783 // Settings shape the whole site's conversion pipeline — restrict
2784 // the save (and the reset) to site admins, not just anyone who can
2785 // open the page via plugin_access_roles.
2786 if (!current_user_can('manage_options')) {
2787 wp_die(__('Sorry, you are not allowed to manage media optimization settings.', 'metasync'));
2788 }
2789
2790 // Handle reset to defaults
2791 if (!empty($_POST['metasync_media_reset'])) {
2792 $defaults = Metasync_Media_Settings::get_defaults();
2793 Metasync_Media_Settings::save_settings($defaults);
2794 $save_success = true;
2795 } elseif (isset($_POST['metasync_media'])) {
2796 $input = wp_unslash($_POST['metasync_media']);
2797 Metasync_Media_Settings::save_settings($input);
2798 $save_success = true;
2799 }
2800 }
2801
2802 // Tab handling
2803 $current_tab = isset($_GET['tab']) ? sanitize_text_field(wp_unslash($_GET['tab'])) : 'settings';
2804
2805 // Prepare image library data
2806 $list_table = null;
2807 $stats = null;
2808 $batch_progress = null;
2809
2810 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-library-list-table.php';
2811 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-batch-optimizer.php';
2812
2813 $list_table = new Metasync_Media_Library_List_Table();
2814 $list_table->prepare_items();
2815
2816 $stats = Metasync_Media_Library_List_Table::get_stats();
2817 $batch_progress = Metasync_Media_Batch_Optimizer::get_progress();
2818
2819 // Render the admin page view
2820 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/views/admin-page.php';
2821 $this->render_layout_close();
2822 }
2823
2824 /**
2825 * Code Minification page callback
2826 */
2827 public function create_admin_code_minification_page()
2828 {
2829 $this->render_layout_open('Code Minification', 'code_minification', 'Minify CSS, JavaScript, and HTML to improve performance.');
2830 // Load settings and compatibility classes
2831 require_once plugin_dir_path(dirname(__FILE__)) . 'code-minification/class-minification-settings.php';
2832 require_once plugin_dir_path(dirname(__FILE__)) . 'code-minification/class-minification-cache.php';
2833 require_once plugin_dir_path(dirname(__FILE__)) . 'code-minification/class-compatibility-guard.php';
2834
2835 $save_success = false;
2836
2837 // Handle form submissions
2838 if (isset($_POST['metasync_code_minification_nonce'])) {
2839 check_admin_referer('metasync_save_code_minification', 'metasync_code_minification_nonce');
2840
2841 // Handle reset to defaults
2842 if (!empty($_POST['metasync_code_min_reset'])) {
2843 $defaults = Metasync_Minification_Settings::get_defaults();
2844 Metasync_Minification_Settings::save_settings($defaults);
2845 $save_success = true;
2846 } elseif (isset($_POST['metasync_code_min'])) {
2847 $input = (array) wp_unslash($_POST['metasync_code_min']);
2848 Metasync_Minification_Settings::save_settings($input);
2849 $save_success = true;
2850 }
2851 }
2852
2853 // Tab handling
2854 $current_tab = isset($_GET['tab']) ? sanitize_text_field(wp_unslash($_GET['tab'])) : 'settings';
2855 $settings = Metasync_Minification_Settings::get_settings();
2856 $conflicts = Metasync_Compatibility_Guard::get_active_conflicts();
2857
2858 // Render the admin page view
2859 require_once plugin_dir_path(dirname(__FILE__)) . 'code-minification/views/admin-page.php';
2860 $this->render_layout_close();
2861 }
2862
2863 // ── Media Optimization AJAX Handlers ──
2864
2865 /**
2866 * AJAX: Optimize a single image.
2867 */
2868 public function ajax_optimize_single_image()
2869 {
2870 check_ajax_referer('metasync_media_opt_nonce', 'nonce');
2871
2872 if (!current_user_can('upload_files')) {
2873 wp_send_json_error(__('Permission denied.', 'metasync'));
2874 }
2875
2876 $attachment_id = isset($_POST['attachment_id']) ? absint($_POST['attachment_id']) : 0;
2877 if (!$attachment_id) {
2878 wp_send_json_error(__('Invalid attachment ID.', 'metasync'));
2879 }
2880
2881 // Ownership: under the replace strategy this permanently deletes the
2882 // attachment's original file, so upload_files alone is not enough —
2883 // the caller must be allowed to edit this specific attachment.
2884 if (!current_user_can('edit_post', $attachment_id)) {
2885 wp_send_json_error(__('Permission denied.', 'metasync'));
2886 }
2887
2888 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-settings.php';
2889 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-image-converter.php';
2890 $settings = Metasync_Media_Settings::get_settings();
2891
2892 $file = get_attached_file($attachment_id);
2893 $mime = get_post_mime_type($attachment_id);
2894
2895 if (!$file || !file_exists($file)) {
2896 wp_send_json_error(__('Optimization failed: file not found.', 'metasync'));
2897 }
2898
2899 $max_bytes = 10 * 1024 * 1024;
2900 if (filesize($file) > $max_bytes) {
2901 $size_mb = round(filesize($file) / 1024 / 1024, 1);
2902 wp_send_json_error(sprintf(
2903 __('Optimization skipped: file size (%s MB) exceeds the 10 MB safety limit to prevent memory issues.', 'metasync'),
2904 $size_mb
2905 ));
2906 }
2907
2908 if (!in_array($mime, ['image/jpeg', 'image/png'], true)) {
2909 wp_send_json_error(__('Optimization failed: unsupported image format.', 'metasync'));
2910 }
2911
2912 $success = Metasync_Image_Converter::convert_attachment($attachment_id, $settings);
2913
2914 if ($success) {
2915 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-library-list-table.php';
2916 wp_send_json_success([
2917 'status_html' => Metasync_Media_Library_List_Table::render_status_html($attachment_id),
2918 'can_revert' => Metasync_Image_Converter::can_revert($attachment_id),
2919 ]);
2920 }
2921
2922 wp_send_json_error(__('Optimization failed: conversion could not be completed.', 'metasync'));
2923 }
2924
2925 /**
2926 * AJAX: Revert a single image.
2927 */
2928 public function ajax_revert_single_image()
2929 {
2930 check_ajax_referer('metasync_media_opt_nonce', 'nonce');
2931
2932 if (!current_user_can('upload_files')) {
2933 wp_send_json_error(__('Permission denied.', 'metasync'));
2934 }
2935
2936 $attachment_id = isset($_POST['attachment_id']) ? absint($_POST['attachment_id']) : 0;
2937 if (!$attachment_id) {
2938 wp_send_json_error(__('Invalid attachment ID.', 'metasync'));
2939 }
2940
2941 // Ownership: reverting deletes the attachment's converted files, so
2942 // restrict it to users who may edit this specific attachment.
2943 if (!current_user_can('edit_post', $attachment_id)) {
2944 wp_send_json_error(__('Permission denied.', 'metasync'));
2945 }
2946
2947 // Replace-strategy conversions have no original to restore — reverting
2948 // would delete the attachment's only file. Refuse up front, like bulk revert does.
2949 if (!Metasync_Image_Converter::can_revert($attachment_id)) {
2950 wp_send_json_error(__('This image cannot be reverted. The original file no longer exists (replace strategy).', 'metasync'));
2951 }
2952
2953 $success = Metasync_Image_Converter::revert_attachment($attachment_id);
2954
2955 if ($success) {
2956 wp_send_json_success();
2957 }
2958
2959 wp_send_json_error(__('Revert failed. Original file may not exist (replace strategy).', 'metasync'));
2960 }
2961
2962 /**
2963 * AJAX: Start batch optimization.
2964 */
2965 public function ajax_start_batch_optimize()
2966 {
2967 check_ajax_referer('metasync_media_opt_nonce', 'nonce');
2968
2969 if (!current_user_can('manage_options')) {
2970 wp_send_json_error(__('Permission denied.', 'metasync'));
2971 }
2972
2973 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-settings.php';
2974 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-batch-optimizer.php';
2975
2976 $settings = Metasync_Media_Settings::get_settings();
2977 $progress = Metasync_Media_Batch_Optimizer::start_batch($settings);
2978
2979 wp_send_json_success($progress);
2980 }
2981
2982 /**
2983 * AJAX: Cancel batch optimization.
2984 */
2985 public function ajax_cancel_batch_optimize()
2986 {
2987 check_ajax_referer('metasync_media_opt_nonce', 'nonce');
2988
2989 if (!current_user_can('manage_options')) {
2990 wp_send_json_error(__('Permission denied.', 'metasync'));
2991 }
2992
2993 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-batch-optimizer.php';
2994 Metasync_Media_Batch_Optimizer::cancel_batch();
2995
2996 wp_send_json_success();
2997 }
2998
2999 /**
3000 * AJAX: Get batch progress + stats.
3001 */
3002 public function ajax_batch_progress()
3003 {
3004 check_ajax_referer('metasync_media_opt_nonce', 'nonce');
3005
3006 // Read-only progress/stats polling — the same capability that can
3007 // open the media optimization page is enough, but a nonce alone
3008 // never was.
3009 if (!current_user_can('upload_files')) {
3010 wp_send_json_error(__('Permission denied.', 'metasync'));
3011 }
3012
3013 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-batch-optimizer.php';
3014 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-library-list-table.php';
3015
3016 $progress = Metasync_Media_Batch_Optimizer::get_progress();
3017 $progress['stats'] = Metasync_Media_Library_List_Table::get_stats();
3018
3019 wp_send_json_success($progress);
3020 }
3021
3022 /**
3023 * AJAX: Bulk optimize selected images.
3024 */
3025 public function ajax_bulk_optimize_selected()
3026 {
3027 check_ajax_referer('metasync_media_opt_nonce', 'nonce');
3028
3029 if (!current_user_can('upload_files')) {
3030 wp_send_json_error(__('Permission denied.', 'metasync'));
3031 }
3032
3033 $ids = isset($_POST['ids']) ? array_map('absint', explode(',', sanitize_text_field(wp_unslash($_POST['ids'])))) : [];
3034 $ids = array_filter($ids);
3035
3036 if (empty($ids)) {
3037 wp_send_json_error(__('No images selected.', 'metasync'));
3038 }
3039
3040 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-settings.php';
3041 $settings = Metasync_Media_Settings::get_settings();
3042
3043 $success = 0;
3044 $failed = 0;
3045 $denied = 0;
3046
3047 foreach ($ids as $id) {
3048 // Per-attachment ownership: replace mode deletes originals, so
3049 // upload_files alone must not allow converting images the caller
3050 // does not own/cannot edit.
3051 if (!current_user_can('edit_post', $id)) {
3052 $denied++;
3053 continue;
3054 }
3055
3056 if (Metasync_Image_Converter::convert_attachment($id, $settings)) {
3057 $success++;
3058 } else {
3059 $failed++;
3060 }
3061 }
3062
3063 wp_send_json_success([
3064 'success' => $success,
3065 'failed' => $failed,
3066 'denied' => $denied,
3067 ]);
3068 }
3069
3070 /**
3071 * AJAX: Bulk unoptimize (revert) selected images.
3072 */
3073 public function ajax_bulk_unoptimize_selected()
3074 {
3075 check_ajax_referer('metasync_media_opt_nonce', 'nonce');
3076
3077 if (!current_user_can('upload_files')) {
3078 wp_send_json_error(__('Permission denied.', 'metasync'));
3079 }
3080
3081 $ids = isset($_POST['ids']) ? array_map('absint', explode(',', sanitize_text_field(wp_unslash($_POST['ids'])))) : [];
3082 $ids = array_filter($ids);
3083
3084 if (empty($ids)) {
3085 wp_send_json_error(__('No images selected.', 'metasync'));
3086 }
3087
3088 $success = 0;
3089 $failed = 0;
3090 $skipped = 0;
3091 $denied = 0;
3092 $errors = [];
3093
3094 foreach ($ids as $id) {
3095 // Per-attachment ownership: reverting deletes the attachment's
3096 // converted files, so restrict it per image, not per role.
3097 if (!current_user_can('edit_post', $id)) {
3098 $denied++;
3099 continue;
3100 }
3101
3102 $format = get_post_meta($id, '_metasync_converted_format', true);
3103
3104 if (!$format) {
3105 $skipped++;
3106 continue;
3107 }
3108
3109 if (!Metasync_Image_Converter::can_revert($id)) {
3110 $skipped++;
3111 $file = get_attached_file($id);
3112 $name = $file ? basename($file) : "ID {$id}";
3113 $errors[] = sprintf(__('%s: skipped — original image unavailable.', 'metasync'), $name);
3114 continue;
3115 }
3116
3117 if (Metasync_Image_Converter::revert_attachment($id)) {
3118 $success++;
3119 } else {
3120 $failed++;
3121 $file = get_attached_file($id);
3122 $name = $file ? basename($file) : "ID {$id}";
3123 $errors[] = sprintf(__('%s: revert failed (original file may not exist).', 'metasync'), $name);
3124 }
3125 }
3126
3127 wp_send_json_success([
3128 'success' => $success,
3129 'failed' => $failed,
3130 'skipped' => $skipped,
3131 'denied' => $denied,
3132 'errors' => $errors,
3133 ]);
3134 }
3135
3136 /**
3137 * AJAX: Process one batch tick (browser-driven chaining).
3138 */
3139 public function ajax_process_batch_tick()
3140 {
3141 check_ajax_referer('metasync_media_opt_nonce', 'nonce');
3142
3143 if (!current_user_can('manage_options')) {
3144 wp_send_json_error(__('Permission denied.', 'metasync'));
3145 }
3146
3147 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-batch-optimizer.php';
3148 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-library-list-table.php';
3149
3150 $progress = Metasync_Media_Batch_Optimizer::process_ajax_tick();
3151 $progress['stats'] = Metasync_Media_Library_List_Table::get_stats();
3152
3153 wp_send_json_success($progress);
3154 }
3155
3156 /**
3157 * AJAX: Delete an orphaned media record (attachment whose file is missing).
3158 *
3159 * Only deletes when the file is confirmed missing on disk, so valid
3160 * attachments can never be removed through this endpoint.
3161 */
3162 public function ajax_delete_orphaned_image()
3163 {
3164 check_ajax_referer('metasync_media_opt_nonce', 'nonce');
3165
3166 $attachment_id = isset($_POST['attachment_id']) ? absint($_POST['attachment_id']) : 0;
3167 if (!$attachment_id) {
3168 wp_send_json_error(__('Invalid attachment ID.', 'metasync'));
3169 }
3170
3171 if (!current_user_can('delete_post', $attachment_id)) {
3172 wp_send_json_error(__('Permission denied.', 'metasync'));
3173 }
3174
3175 if (get_post_type($attachment_id) !== 'attachment') {
3176 wp_send_json_error(__('Not an attachment.', 'metasync'));
3177 }
3178
3179 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-library-list-table.php';
3180
3181 // Guard: only orphaned records (missing file) may be deleted here.
3182 if (!Metasync_Media_Library_List_Table::is_file_missing($attachment_id)) {
3183 wp_send_json_error(__('The image file exists; refusing to delete a valid attachment.', 'metasync'));
3184 }
3185
3186 $deleted = wp_delete_attachment($attachment_id, true);
3187
3188 if ($deleted) {
3189 wp_send_json_success([
3190 'stats' => Metasync_Media_Library_List_Table::get_stats(),
3191 ]);
3192 }
3193
3194 wp_send_json_error(__('Failed to delete the orphaned media record.', 'metasync'));
3195 }
3196
3197 /**
3198 * Cron handler: Process batch optimization tick.
3199 */
3200 public function handle_media_batch_cron()
3201 {
3202 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-settings.php';
3203 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-batch-optimizer.php';
3204
3205 Metasync_Media_Batch_Optimizer::process_batch_tick();
3206 }
3207
3208 /**
3209 * Report Issue page callback
3210 */
3211 public function create_admin_report_issue_page()
3212 {
3213 // Load the Report Issue view
3214 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-report-issue.php';
3215 }
3216
3217 /**
3218 * XML Sitemap page callback
3219 */
3220 public function create_admin_xml_sitemap_page()
3221 {
3222 // Load sitemap generator class
3223 require_once plugin_dir_path(dirname(__FILE__)) . 'sitemap/class-metasync-sitemap-generator.php';
3224
3225 $sitemap_generator = new Metasync_Sitemap_Generator();
3226
3227 // Determine active tab (from GET param or POST redirect)
3228 $active_tab = 'general';
3229 if (isset($_GET['tab']) && in_array($_GET['tab'], ['general', 'news', 'video'], true)) {
3230 $active_tab = sanitize_text_field(wp_unslash($_GET['tab']));
3231 } elseif (isset($_POST['redirect_tab']) && in_array($_POST['redirect_tab'], ['general', 'news', 'video'], true)) {
3232 $active_tab = sanitize_text_field(wp_unslash($_POST['redirect_tab']));
3233 }
3234
3235 // Handle Main Sitemap settings form submission
3236 if (isset($_POST['metasync_sitemap_settings_nonce']) && isset($_POST['save_sitemap_settings'])) {
3237 check_admin_referer('metasync_sitemap_settings_action', 'metasync_sitemap_settings_nonce');
3238
3239 $sitemap_settings = [
3240 '_configured' => true,
3241 'post_types' => array_map('sanitize_key', (array) ($_POST['sitemap_post_types'] ?? [])),
3242 'categories' => array_map('absint', (array) ($_POST['sitemap_categories'] ?? [])),
3243 'tags' => array_map('absint', (array) ($_POST['sitemap_tags'] ?? [])),
3244 'taxonomies' => array_map('sanitize_key', (array) ($_POST['sitemap_taxonomies'] ?? [])),
3245 'excluded_urls' => sanitize_textarea_field(wp_unslash($_POST['sitemap_excluded_urls'] ?? '')),
3246 ];
3247
3248 update_option('metasync_sitemap_settings', $sitemap_settings);
3249
3250 // Regenerate sitemap with new content settings
3251 $result = $sitemap_generator->generate_sitemap();
3252 if (is_wp_error($result)) {
3253 echo '<div class="notice notice-error"><p>' . esc_html(
3254 sprintf(__('Settings saved but sitemap generation failed: %s', 'metasync'), $result->get_error_message())
3255 ) . '</p></div>';
3256 } elseif (false === $result) {
3257 echo '<div class="notice notice-error"><p>' . esc_html__('Settings saved but sitemap generation failed: the sitemap data could not be stored, so no sitemap is being served. Check your object cache and database write settings, then try again.', 'metasync') . '</p></div>';
3258 } else {
3259 echo '<div class="notice notice-success"><p>' . esc_html__('Sitemap content settings saved and sitemap regenerated!', 'metasync') . '</p></div>';
3260 }
3261 }
3262
3263 // Handle News Sitemap settings form submission
3264 if (isset($_POST['metasync_news_sitemap_nonce']) && isset($_POST['save_news_sitemap'])) {
3265 check_admin_referer('metasync_news_sitemap_action', 'metasync_news_sitemap_nonce');
3266
3267 // Build generic taxonomy filters
3268 $news_taxonomies = [];
3269 if (!empty($_POST['news_taxonomies']) && is_array($_POST['news_taxonomies'])) {
3270 foreach ($_POST['news_taxonomies'] as $tax_name => $term_ids) {
3271 $news_taxonomies[sanitize_key($tax_name)] = array_map('absint', (array) $term_ids);
3272 }
3273 }
3274
3275 $news_settings = [
3276 'enabled' => isset($_POST['news_enabled']),
3277 'post_types' => array_map('sanitize_key', (array) ($_POST['news_post_types'] ?? ['post'])),
3278 'categories' => array_map('absint', (array) ($_POST['news_categories'] ?? [])),
3279 'tags' => array_map('absint', (array) ($_POST['news_tags'] ?? [])),
3280 'taxonomies' => $news_taxonomies,
3281 'excluded_urls' => sanitize_textarea_field(wp_unslash($_POST['news_excluded_urls'] ?? '')),
3282 'publication_name' => sanitize_text_field(wp_unslash($_POST['publication_name'] ?? '')),
3283 'publication_language' => sanitize_text_field(wp_unslash($_POST['publication_language'] ?? '')),
3284 ];
3285
3286 // Always invalidate old cache before saving new settings
3287 delete_transient('metasync_vsm_' . md5('news-sitemap.xml'));
3288
3289 update_option('metasync_news_sitemap_settings', $news_settings);
3290
3291 if ($news_settings['enabled']) {
3292 $sitemap_generator->generate_news_sitemap();
3293 update_option('metasync_sitemap_last_generated', current_time('mysql'));
3294 echo '<div class="notice notice-success"><p>' . esc_html__('News sitemap settings saved and sitemap regenerated!', 'metasync') . '</p></div>';
3295 } else {
3296 // Also remove physical file if it exists
3297 if (file_exists(ABSPATH . 'news-sitemap.xml')) {
3298 @unlink(ABSPATH . 'news-sitemap.xml');
3299 }
3300 echo '<div class="notice notice-success"><p>' . esc_html__('News sitemap settings saved. Sitemap cache cleared.', 'metasync') . '</p></div>';
3301 }
3302 }
3303
3304 // Handle Video Sitemap settings form submission
3305 if (isset($_POST['metasync_video_sitemap_nonce']) && isset($_POST['save_video_sitemap'])) {
3306 check_admin_referer('metasync_video_sitemap_action', 'metasync_video_sitemap_nonce');
3307
3308 // Build generic taxonomy filters
3309 $video_taxonomies = [];
3310 if (!empty($_POST['video_taxonomies']) && is_array($_POST['video_taxonomies'])) {
3311 foreach ($_POST['video_taxonomies'] as $tax_name => $term_ids) {
3312 $video_taxonomies[sanitize_key($tax_name)] = array_map('absint', (array) $term_ids);
3313 }
3314 }
3315
3316 $video_settings = [
3317 'enabled' => isset($_POST['video_enabled']),
3318 'post_types' => array_map('sanitize_key', (array) ($_POST['video_post_types'] ?? ['post', 'page'])),
3319 'auto_detect' => isset($_POST['auto_detect']),
3320 'taxonomies' => $video_taxonomies,
3321 'excluded_urls' => sanitize_textarea_field(wp_unslash($_POST['video_excluded_urls'] ?? '')),
3322 // This array replaces the stored option wholesale, so every
3323 // setting must be represented here or it is wiped on save.
3324 'video_url_meta_keys' => sanitize_textarea_field(wp_unslash($_POST['video_url_meta_keys'] ?? '')),
3325 'video_thumbnail_meta_keys' => sanitize_textarea_field(wp_unslash($_POST['video_thumbnail_meta_keys'] ?? '')),
3326 ];
3327
3328 // Always invalidate old cache before saving new settings
3329 delete_transient('metasync_vsm_' . md5('video-sitemap.xml'));
3330
3331 update_option('metasync_video_sitemap_settings', $video_settings);
3332
3333 if ($video_settings['enabled']) {
3334 $sitemap_generator->generate_video_sitemap();
3335 update_option('metasync_sitemap_last_generated', current_time('mysql'));
3336 echo '<div class="notice notice-success"><p>' . esc_html__('Video sitemap settings saved and sitemap regenerated!', 'metasync') . '</p></div>';
3337 } else {
3338 // Also remove physical file if it exists
3339 if (file_exists(ABSPATH . 'video-sitemap.xml')) {
3340 @unlink(ABSPATH . 'video-sitemap.xml');
3341 }
3342 echo '<div class="notice notice-success"><p>' . esc_html__('Video sitemap settings saved. Sitemap cache cleared.', 'metasync') . '</p></div>';
3343 }
3344 }
3345
3346 // Handle News Sitemap generate action
3347 if (isset($_POST['metasync_news_sitemap_nonce']) && isset($_POST['generate_news_sitemap'])) {
3348 check_admin_referer('metasync_news_sitemap_action', 'metasync_news_sitemap_nonce');
3349 $result = $sitemap_generator->generate_news_sitemap();
3350 if ($result) {
3351 echo '<div class="notice notice-success"><p>' . esc_html__('News sitemap generated successfully!', 'metasync') . '</p></div>';
3352 } else {
3353 echo '<div class="notice notice-error"><p>' . esc_html__('News sitemap generation failed. Check if it is enabled and no conflicting plugins are active.', 'metasync') . '</p></div>';
3354 }
3355 }
3356
3357 // Handle Video Sitemap generate action
3358 if (isset($_POST['metasync_video_sitemap_nonce']) && isset($_POST['generate_video_sitemap'])) {
3359 check_admin_referer('metasync_video_sitemap_action', 'metasync_video_sitemap_nonce');
3360 $result = $sitemap_generator->generate_video_sitemap();
3361 if ($result) {
3362 echo '<div class="notice notice-success"><p>' . esc_html__('Video sitemap generated successfully!', 'metasync') . '</p></div>';
3363 } else {
3364 echo '<div class="notice notice-error"><p>' . esc_html__('Video sitemap generation failed. Check if it is enabled and no conflicting plugins are active.', 'metasync') . '</p></div>';
3365 }
3366 }
3367
3368 // Handle form submissions
3369 if (isset($_POST['metasync_sitemap_nonce'])) {
3370 check_admin_referer('metasync_sitemap_action', 'metasync_sitemap_nonce');
3371
3372 if (isset($_POST['generate_sitemap'])) {
3373 // Auto-disable other sitemap generators before generating
3374 $disabled_plugins = $sitemap_generator->disable_other_sitemap_generators();
3375
3376 // Generate news/video sitemaps FIRST so they exist when the main sitemap builds its index
3377 $news_opts = get_option('metasync_news_sitemap_settings', []);
3378 $video_opts = get_option('metasync_video_sitemap_settings', []);
3379 $extras = [];
3380 if (!empty($news_opts['enabled'])) {
3381 if ($sitemap_generator->generate_news_sitemap()) {
3382 $extras[] = 'news';
3383 }
3384 }
3385
3386 // Reclaim unreferenced memory between the news and video passes
3387 // so the video generator starts with more headroom (they share
3388 // one request). Each generator already releases its own object
3389 // cache via clean_post_cache(), so no cache flush is needed here.
3390 if (function_exists('gc_collect_cycles')) {
3391 gc_collect_cycles();
3392 }
3393
3394 if (!empty($video_opts['enabled'])) {
3395 if ($sitemap_generator->generate_video_sitemap()) {
3396 $extras[] = 'video';
3397 }
3398 }
3399
3400 // Same inter-pass GC before the main sitemap pass begins.
3401 if (function_exists('gc_collect_cycles')) {
3402 gc_collect_cycles();
3403 }
3404
3405 // Generate main sitemap (its index will include news/video since they now exist)
3406 $result = $sitemap_generator->generate_sitemap();
3407
3408 if (is_wp_error($result)) {
3409 $error_msg = $result->get_error_message();
3410 error_log('[MetaSync] Sitemap generation failed: ' . $error_msg);
3411 echo '<div class="notice notice-error"><p>' . esc_html(
3412 sprintf(__('Sitemap generation failed: %s', 'metasync'), $error_msg)
3413 ) . '</p></div>';
3414 } elseif (false === $result) {
3415 echo '<div class="notice notice-error"><p>' . esc_html__('Sitemap generation failed: the sitemap data could not be stored, so no sitemap is being served. Check your object cache and database write settings, then try again.', 'metasync') . '</p></div>';
3416 } else {
3417 $message = esc_html__('Sitemap generated successfully!', 'metasync');
3418 if (!empty($extras)) {
3419 $message .= ' ' . sprintf(
3420 esc_html__('Also generated %s sitemap(s).', 'metasync'),
3421 implode(' & ', $extras)
3422 );
3423 }
3424 if ($disabled_plugins) {
3425 $message .= ' ' . esc_html__('Conflicting sitemap generators have been automatically disabled.', 'metasync');
3426 }
3427
3428 // Check if robots.txt was updated
3429 $robots_result = get_transient('metasync_sitemap_robots_updated');
3430 if ($robots_result && $robots_result['success']) {
3431 if ($robots_result['action'] === 'added') {
3432 $message .= ' ' . esc_html__('Sitemap URL has been added to robots.txt.', 'metasync');
3433 } elseif ($robots_result['action'] === 'updated') {
3434 $message .= ' ' . esc_html__('Sitemap URL has been updated in robots.txt.', 'metasync');
3435 } elseif ($robots_result['action'] === 'created') {
3436 $message .= ' ' . esc_html__('robots.txt file has been created with sitemap URL.', 'metasync');
3437 }
3438 delete_transient('metasync_sitemap_robots_updated');
3439 }
3440
3441 echo '<div class="notice notice-success"><p>' . esc_html($message) . '</p></div>';
3442 }
3443 } elseif (isset($_POST['enable_auto_update'])) {
3444 update_option('metasync_sitemap_auto_update', true);
3445 $sitemap_generator->setup_auto_update_hooks();
3446 echo '<div class="notice notice-success"><p>' . esc_html__('Auto-update enabled!', 'metasync') . '</p></div>';
3447 } elseif (isset($_POST['disable_auto_update'])) {
3448 update_option('metasync_sitemap_auto_update', false);
3449 echo '<div class="notice notice-success"><p>' . esc_html__('Auto-update disabled!', 'metasync') . '</p></div>';
3450 } elseif (isset($_POST['delete_general_sitemap'])) {
3451 $deleted = $sitemap_generator->delete_sitemap('general');
3452
3453 if ($deleted) {
3454 // Disable auto-update only when the general sitemap is removed
3455 update_option('metasync_sitemap_auto_update', false);
3456 // Re-enable WP core sitemap only if no other MetaSync sitemaps remain
3457 if (!$sitemap_generator->sitemap_exists()) {
3458 delete_option('metasync_disable_wp_sitemap');
3459 }
3460 echo '<div class="notice notice-success"><p>' . esc_html__('General sitemap deleted successfully!', 'metasync') . '</p></div>';
3461 } else {
3462 echo '<div class="notice notice-error"><p>' . esc_html__('Failed to delete general sitemap. The files may not exist or are not writable.', 'metasync') . '</p></div>';
3463 }
3464 } elseif (isset($_POST['delete_news_sitemap'])) {
3465 $deleted = $sitemap_generator->delete_sitemap('news');
3466
3467 if ($deleted) {
3468 echo '<div class="notice notice-success"><p>' . esc_html__('News sitemap deleted successfully!', 'metasync') . '</p></div>';
3469 } else {
3470 echo '<div class="notice notice-error"><p>' . esc_html__('Failed to delete news sitemap. The file may not exist or is not writable.', 'metasync') . '</p></div>';
3471 }
3472 } elseif (isset($_POST['delete_video_sitemap'])) {
3473 $deleted = $sitemap_generator->delete_sitemap('video');
3474
3475 if ($deleted) {
3476 echo '<div class="notice notice-success"><p>' . esc_html__('Video sitemap deleted successfully!', 'metasync') . '</p></div>';
3477 } else {
3478 echo '<div class="notice notice-error"><p>' . esc_html__('Failed to delete video sitemap. The file may not exist or is not writable.', 'metasync') . '</p></div>';
3479 }
3480 } elseif (isset($_POST['delete_sitemap'])) {
3481 // Delete all sitemaps: main + news + video (handled by delete_sitemap)
3482 $deleted = $sitemap_generator->delete_sitemap();
3483
3484 if ($deleted) {
3485 // Also disable auto-update when deleting
3486 update_option('metasync_sitemap_auto_update', false);
3487 // Re-enable WP core sitemap so the site isn't left with zero sitemaps
3488 delete_option('metasync_disable_wp_sitemap');
3489 echo '<div class="notice notice-success"><p>' . esc_html__('All sitemaps deleted successfully!', 'metasync') . '</p></div>';
3490 } else {
3491 echo '<div class="notice notice-error"><p>' . esc_html__('Failed to delete sitemaps. The files may not exist or are not writable.', 'metasync') . '</p></div>';
3492 }
3493 } elseif (isset($_POST['enable_other_sitemaps'])) {
3494 // Re-enable other sitemap plugins
3495 $enabled_plugins = $sitemap_generator->enable_other_sitemap_generators();
3496 if ($enabled_plugins) {
3497 echo '<div class="notice notice-success"><p>' . esc_html__('Other sitemap plugins have been re-enabled successfully!', 'metasync') . '</p></div>';
3498 } else {
3499 echo '<div class="notice notice-info"><p>' . esc_html__('No sitemap plugins were found to re-enable.', 'metasync') . '</p></div>';
3500 }
3501 }
3502 }
3503
3504 // Get sitemap info
3505 $sitemap_exists = $sitemap_generator->sitemap_exists();
3506 $sitemap_url = $sitemap_generator->get_sitemap_url();
3507 $url_count = $sitemap_generator->count_urls();
3508 $last_generated = $sitemap_generator->get_last_generated_time();
3509 $auto_update_enabled = get_option('metasync_sitemap_auto_update', false);
3510 $active_sitemap_plugins = $sitemap_generator->check_active_sitemap_plugins();
3511
3512 // Main sitemap content settings
3513 $sitemap_settings = get_option('metasync_sitemap_settings', [
3514 'post_types' => [],
3515 'categories' => [],
3516 'tags' => [],
3517 'taxonomies' => [],
3518 'excluded_urls' => '',
3519 ]);
3520
3521 // News and video sitemap settings for tabs
3522 $news_settings = get_option('metasync_news_sitemap_settings', [
3523 'enabled' => false,
3524 'post_types' => ['post'],
3525 'categories' => [],
3526 'tags' => [],
3527 'taxonomies' => [],
3528 'excluded_urls' => '',
3529 'publication_name' => '',
3530 'publication_language' => '',
3531 ]);
3532 $video_settings = get_option('metasync_video_sitemap_settings', [
3533 'enabled' => false,
3534 'post_types' => ['post', 'page'],
3535 'auto_detect' => true,
3536 'taxonomies' => [],
3537 'excluded_urls' => '',
3538 ]);
3539
3540 // Load view
3541 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-xml-sitemap.php';
3542 }
3543
3544 /**
3545 * Custom Pages page callback
3546 */
3547 public function create_admin_custom_pages_page()
3548 {
3549 Metasync_Admin_Pages::get_instance($this)->create_admin_custom_pages_page();
3550 }
3551
3552 /**
3553 * 404 Monitor page callback
3554 */
3555 public function create_admin_404_monitor_page()
3556 {
3557 Metasync_Admin_Pages::get_instance($this)->create_admin_404_monitor_page();
3558 }
3559
3560 /**
3561 * SEO Health dashboard page callback
3562 */
3563 public function create_admin_seo_health_page()
3564 {
3565 require_once plugin_dir_path(__FILE__) . 'class-metasync-seo-health.php';
3566 Metasync_SEO_Health::get_instance()->render_page();
3567 }
3568
3569 /**
3570 * Site Verification page callback
3571 */
3572 public function create_admin_search_engine_verification_page()
3573 {
3574 Metasync_Admin_Pages::get_instance($this)->create_admin_search_engine_verification_page();
3575 }
3576
3577 /**
3578 * Local Business page callback
3579 */
3580 public function create_admin_local_business_page()
3581 {
3582 Metasync_Admin_Pages::get_instance($this)->create_admin_local_business_page();
3583 }
3584
3585 /**
3586 * Code Snippets page callback
3587 */
3588 public function create_admin_code_snippets_page()
3589 {
3590 Metasync_Admin_Pages::get_instance($this)->create_admin_code_snippets_page();
3591 }
3592
3593 /**
3594 * Schema Markup settings page callback
3595 */
3596 public function create_admin_schema_markup_page()
3597 {
3598 Metasync_Admin_Pages::get_instance($this)->create_admin_schema_markup_page();
3599 }
3600
3601 /**
3602 * Breadcrumbs settings page callback
3603 */
3604 public function create_admin_breadcrumbs_page()
3605 {
3606 Metasync_Admin_Pages::get_instance($this)->create_admin_breadcrumbs_page();
3607 }
3608
3609 /**
3610 * Google Instant Index Setting page callback
3611 */
3612 public function create_admin_google_instant_index_page()
3613 {
3614 $this->render_layout_open('Instant Indexing', 'instant_index', 'Submit URLs to Google for instant indexing via the Indexing API.');
3615
3616 // Render shared Google Index credentials section
3617 if (!function_exists('google_index_direct')) {
3618 if (file_exists(plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php')) {
3619 require_once plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php';
3620 } else {
3621 error_log('MetaSync Google Index: google-index-init.php not found at ' . plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php');
3622 return;
3623 }
3624 }
3625 $google_index = google_index_direct();
3626 $service_info = $google_index->get_service_account_info();
3627 $is_configured = !isset($service_info['error']);
3628
3629 $saved_json_display = $is_configured ? $google_index->get_redacted_config_json() : '';
3630
3631 include plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-index-api-settings.php';
3632
3633 // Render post types selection with save form
3634 $options = get_option('metasync_options_instant_indexing', ['post_types' => []]);
3635 $post_types_settings = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : [];
3636 ?>
3637 <form method="POST" action="">
3638 <?php wp_nonce_field('metasync_instant_indexing_settings', 'metasync_instant_indexing_nonce'); ?>
3639 <?php include plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-instant-post-types.php'; ?>
3640 <div class="dashboard-card" style="padding: 20px;">
3641 <?php submit_button('Save Post Types', 'primary', 'submit', false, array('class' => 'button button-primary')); ?>
3642 </div>
3643 </form>
3644 <?php
3645
3646 $this->render_layout_close();
3647 }
3648
3649 /**
3650 * Google Console page callback
3651 */
3652 public function create_admin_google_console_page()
3653 {
3654 $this->render_layout_open('Google Console', 'google_console', 'View Google Search Console data and manage indexing requests.');
3655 $service_info = function_exists('google_index_direct') ? google_index_direct()->get_service_account_info() : ['error' => 'Module not loaded'];
3656 $is_configured = !isset($service_info['error']);
3657 include_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-console.php';
3658 $this->render_layout_close();
3659 }
3660
3661 /**
3662 * Bing Console page callback
3663 */
3664 public function create_admin_bing_console_page()
3665 {
3666 $this->render_layout_open('Bing Console', 'bing_console', 'Submit URLs to Bing for instant indexing via IndexNow.');
3667 require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php';
3668 $bing_instant_index = new Metasync_Bing_Instant_Index();
3669 $bing_instant_index->show_bing_instant_indexing_console();
3670 $this->render_layout_close();
3671 }
3672
3673 /**
3674 * Global Options page callback
3675 */
3676 public function create_admin_global_settings_page()
3677 {
3678 Metasync_Admin_Pages::get_instance($this)->create_admin_global_settings_page();
3679 }
3680
3681 /**
3682 * Common Meta Options page callback
3683 */
3684 public function create_admin_common_meta_settings_page()
3685 {
3686 Metasync_Admin_Pages::get_instance($this)->create_admin_common_meta_settings_page();
3687 }
3688
3689 /**
3690 * Social meta page callback
3691 */
3692 public function create_admin_social_meta_page()
3693 {
3694 Metasync_Admin_Pages::get_instance($this)->create_admin_social_meta_page();
3695 }
3696
3697
3698 /**
3699 * Indexation Control page callback
3700 */
3701 public function create_admin_seo_controls_page()
3702 {
3703 Metasync_Admin_Pages::get_instance($this)->create_admin_seo_controls_page();
3704 }
3705
3706 /**
3707 * redirection page callback with tabs
3708 */
3709 public function create_admin_redirections_page()
3710 {
3711 Metasync_Redirections_Admin::get_instance($this->db_redirection, $this)->create_admin_redirections_page();
3712 }
3713
3714 /**
3715 * Display transient error/success messages for redirections
3716 */
3717 public function display_redirection_messages()
3718 {
3719 Metasync_Redirections_Admin::get_instance($this->db_redirection, $this)->display_redirection_messages();
3720 }
3721
3722 /**
3723 * Display admin notice when batch processing was deferred due to high CPU load.
3724 * Reads transient set by Metasync_CPU_Monitor::record_deferral() and clears it.
3725 */
3726 public function display_cpu_deferral_notice()
3727 {
3728 $data = get_transient( Metasync_CPU_Monitor::DEFER_NOTICE_TRANSIENT );
3729 if ( ! $data || ! is_array( $data ) ) {
3730 return;
3731 }
3732 delete_transient( Metasync_CPU_Monitor::DEFER_NOTICE_TRANSIENT );
3733 echo '<div class="notice notice-warning is-dismissible"><p>';
3734 printf(
3735 /* translators: 1: plugin name, 2: current load, 3: threshold, 4: core count */
3736 esc_html__( '%1$s: Batch processing was deferred — server CPU load (%2$s) exceeded the threshold (%3$s on %4$s cores). Processing will resume automatically.', 'metasync' ),
3737 esc_html( Metasync::get_effective_plugin_name() ),
3738 esc_html( $data['load'] ),
3739 esc_html( $data['threshold'] ),
3740 esc_html( $data['cores'] )
3741 );
3742 echo '</p></div>';
3743 }
3744
3745 /**
3746 * Display info notice when another SEO plugin also generates /llms.txt.
3747 *
3748 * MetaSync always serves its own version when enabled (priority 1). This
3749 * notice simply informs the admin that another plugin was detected.
3750 */
3751 public function display_llms_txt_conflict_notice()
3752 {
3753 if (!get_transient('metasync_llms_conflict')) {
3754 return;
3755 }
3756 echo '<div class="notice notice-info is-dismissible"><p>';
3757 echo esc_html(sprintf(__('Note: Another SEO plugin (Yoast, Rank Math, or AIOSEO) may also be generating /llms.txt. %s\'s version takes priority when enabled.', 'metasync'), Metasync::get_effective_plugin_name()));
3758 echo '</p></div>';
3759 }
3760
3761 /**
3762 * AJAX handler for updating database structure
3763 */
3764 public function ajax_update_db_structure()
3765 {
3766 Metasync_Admin_Ajax::instance()->ajax_update_db_structure();
3767 }
3768
3769 /**
3770 * AJAX handler to save wizard progress
3771 *
3772 * @since 1.0.0
3773 */
3774 public function ajax_save_wizard_progress()
3775 {
3776 Metasync_Admin_Ajax::instance()->ajax_save_wizard_progress();
3777 }
3778
3779 /**
3780 * AJAX handler to complete wizard
3781 *
3782 * @since 1.0.0
3783 */
3784 public function ajax_complete_wizard()
3785 {
3786 Metasync_Admin_Ajax::instance()->ajax_complete_wizard();
3787 }
3788
3789 /**
3790 * AJAX handler to validate robots.txt content
3791 */
3792 public function ajax_validate_robots()
3793 {
3794 Metasync_Admin_Ajax::instance()->ajax_validate_robots();
3795 }
3796
3797 /**
3798 * AJAX handler to get default robots.txt content
3799 */
3800 public function ajax_get_default_robots()
3801 {
3802 Metasync_Admin_Ajax::instance()->ajax_get_default_robots();
3803 }
3804
3805 /**
3806 * AJAX handler to preview robots.txt backup content
3807 */
3808 public function ajax_preview_robots_backup()
3809 {
3810 Metasync_Admin_Ajax::instance()->ajax_preview_robots_backup();
3811 }
3812
3813 /**
3814 * AJAX handler to delete robots.txt backup
3815 */
3816 public function ajax_delete_robots_backup()
3817 {
3818 Metasync_Admin_Ajax::instance()->ajax_delete_robots_backup();
3819 }
3820
3821 /**
3822 * AJAX handler to restore robots.txt backup
3823 */
3824 public function ajax_restore_robots_backup()
3825 {
3826 Metasync_Admin_Ajax::instance()->ajax_restore_robots_backup();
3827 }
3828
3829 /**
3830 * AJAX handler to fetch a paginated page of robots.txt backups
3831 */
3832 public function ajax_get_robots_backups()
3833 {
3834 Metasync_Admin_Ajax::instance()->ajax_get_robots_backups();
3835 }
3836
3837 public function ajax_create_redirect_from_404()
3838 {
3839 Metasync_Admin_Ajax::instance()->ajax_create_redirect_from_404();
3840 }
3841
3842 /**
3843 * AJAX handler for testing host blocking with GET request
3844 */
3845 public function ajax_test_host_blocking_get()
3846 {
3847 Metasync_Admin_Ajax::instance()->ajax_test_host_blocking_get();
3848 }
3849
3850 /**
3851 * AJAX handler for testing host blocking with POST request
3852 */
3853 public function ajax_test_host_blocking_post()
3854 {
3855 Metasync_Admin_Ajax::instance()->ajax_test_host_blocking_post();
3856 }
3857
3858 /**
3859 * Register REST API endpoint for ping
3860 */
3861 public function register_ping_rest_endpoint()
3862 {
3863 register_rest_route('metasync/v1', '/ping', array(
3864 'methods' => array('GET', 'POST'),
3865 'callback' => array($this, 'handle_ping_rest_endpoint'),
3866 'permission_callback' => '__return_true', // Allow public access
3867 'args' => array(
3868 'test' => array(
3869 'description' => 'Optional test parameter',
3870 'type' => 'string',
3871 'sanitize_callback' => 'sanitize_text_field',
3872 ),
3873 ),
3874 ));
3875 }
3876
3877 /**
3878 * Handle REST API ping endpoint
3879 */
3880 public function handle_ping_rest_endpoint($request)
3881 {
3882 // Get request method
3883 $method = $request->get_method();
3884
3885 // Prepare response data
3886 $response_data = array(
3887 'response' => 'pong',
3888 'method' => $method,
3889 'timestamp' => current_time('mysql'),
3890 'site_url' => home_url(),
3891 'plugin_version' => METASYNC_VERSION
3892 );
3893
3894 // Add request data for POST requests
3895 if ($method === 'POST') {
3896 $body = $request->get_body();
3897 if (!empty($body)) {
3898 $response_data['received_data'] = json_decode($body, true);
3899 }
3900
3901 // Add any query parameters
3902 $params = $request->get_params();
3903 if (!empty($params)) {
3904 $response_data['query_params'] = $params;
3905 }
3906 }
3907
3908 // Add test parameter if provided
3909 $test_param = $request->get_param('test');
3910 if (!empty($test_param)) {
3911 $response_data['test_param'] = $test_param;
3912 }
3913
3914 return new WP_REST_Response($response_data, 200);
3915 }
3916
3917 /**
3918 * Site error logs page callback
3919 */
3920
3921 public function create_admin_error_logs_page()
3922 {
3923 Metasync_Admin_Pages::get_instance($this)->create_admin_error_logs_page();
3924 }
3925
3926 /**
3927 * Compatibility page callback
3928 */
3929 public function create_admin_compatibility_page()
3930 {
3931 Metasync_Compatibility_Checker::instance()->create_admin_compatibility_page($this);
3932 }
3933
3934 /**
3935 * Sync Log page callback
3936 */
3937 public function create_admin_sync_log_page()
3938 {
3939 // Classes are now autoloaded
3940
3941 $sync_db = new Metasync_Sync_History_Database();
3942
3943 // Handle AJAX requests for sync log data
3944 if (wp_doing_ajax()) {
3945 $this->handle_sync_log_ajax();
3946 return;
3947 }
3948
3949 // Get pagination parameters
3950 $page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
3951 $per_page = Metasync_Per_Page_Helper::resolve('sync_log', 10);
3952 $offset = ($page - 1) * $per_page;
3953
3954 // Get filters
3955 $filters = [
3956 // UI exposes date_range and status only. We compute date_from/date_to based on date_range
3957 'date_range' => isset($_GET['date_range']) ? sanitize_text_field(wp_unslash($_GET['date_range'])) : '',
3958 'status' => isset($_GET['status']) ? sanitize_text_field(wp_unslash($_GET['status'])) : '',
3959 ];
3960
3961 // Map date_range to concrete date_from/date_to for DB queries
3962 $date_range = $filters['date_range'];
3963 $wp_now_ts = current_time('timestamp');
3964 $date_from = '';
3965 $date_to = '';
3966
3967 if (!empty($date_range)) {
3968 // End boundary is now by default
3969 $date_to = date('Y-m-d H:i:s', $wp_now_ts);
3970
3971 if ($date_range === 'today') {
3972 $start_ts = strtotime('today', $wp_now_ts);
3973 $date_from = date('Y-m-d H:i:s', $start_ts);
3974 } elseif ($date_range === 'yesterday') {
3975 $start_ts = strtotime('yesterday', $wp_now_ts);
3976 $end_ts = strtotime('today', $wp_now_ts) - 1; // end of yesterday
3977 $date_from = date('Y-m-d H:i:s', $start_ts);
3978 $date_to = date('Y-m-d H:i:s', $end_ts);
3979 } elseif ($date_range === 'this_week') {
3980 $start_of_week = (int) get_option('start_of_week', 1); // 0=Sun, 1=Mon
3981 $day_of_week = (int) date('w', $wp_now_ts); // 0=Sun..6=Sat
3982 // Convert start_of_week to PHP's 0..6 where 0=Sunday
3983 $delta_days = ($day_of_week - $start_of_week + 7) % 7;
3984 $start_ts = strtotime('-' . $delta_days . ' days', strtotime('today', $wp_now_ts));
3985 $date_from = date('Y-m-d H:i:s', $start_ts);
3986 } elseif ($date_range === 'this_month') {
3987 $start_ts = strtotime(date('Y-m-01 00:00:00', $wp_now_ts));
3988 $date_from = date('Y-m-d H:i:s', $start_ts);
3989 } elseif ($date_range === 'all') {
3990 // no bounds
3991 }
3992 }
3993
3994 if (!empty($date_from)) {
3995 $filters['date_from'] = $date_from;
3996 }
3997 if (!empty($date_to)) {
3998 $filters['date_to'] = $date_to;
3999 }
4000
4001 // Remove empty filters
4002 $filters = array_filter($filters);
4003
4004 // Get sync history records
4005 $sync_records = $sync_db->getAllRecords($per_page, $offset, $filters);
4006 $total_records = $sync_db->get_count($filters);
4007 $total_pages = ceil($total_records / $per_page);
4008
4009 // Get statistics
4010 $stats = $sync_db->get_statistics();
4011
4012 $this->render_layout_open('Changes Log', 'sync_log', 'Track recent content synchronizations and changes from external tools.');
4013 ?>
4014 <div class="dashboard-card">
4015 <div class="sync-log-header">
4016 <div class="sync-log-title-section">
4017 <h2>Changes Log</h2>
4018 <p style="color: var(--dashboard-text-secondary); margin-bottom: 0;">
4019 Recent content synchronizations from external tools.
4020 <span style="margin-left:8px; font-size:12px; opacity:.75;">Records are automatically removed after 90 days.</span>
4021 </p>
4022 </div>
4023
4024 <!-- Filters + Clear Log button - Right aligned -->
4025 <div class="sync-log-filters" style="display:flex;align-items:center;gap:10px;">
4026 <button type="button" id="metasync-clear-sync-log-btn"
4027 style="background:#dc3545;color:#fff;border:none;padding:6px 14px;border-radius:4px;cursor:pointer;font-size:13px;"
4028 data-nonce="<?php echo esc_attr(wp_create_nonce('metasync_clear_sync_log')); ?>">
4029 🗑 Clear Log
4030 </button>
4031 <form method="get" class="sync-filters-form" onchange="this.submit()" style="display:flex;flex-direction:row;align-items:center;gap:12px;flex-wrap:nowrap;">
4032 <input type="hidden" name="page" value="<?php echo esc_attr($_GET['page']); ?>">
4033
4034 <select name="date_range" class="sync-filter-select">
4035 <option value="all" <?php selected($filters['date_range'] ?? 'all', 'all'); ?>> All Time</option>
4036 <option value="today" <?php selected($filters['date_range'] ?? '', 'today'); ?>>Today</option>
4037 <option value="yesterday" <?php selected($filters['date_range'] ?? '', 'yesterday'); ?>>Yesterday</option>
4038 <option value="this_week" <?php selected($filters['date_range'] ?? '', 'this_week'); ?>>This week</option>
4039 <option value="this_month" <?php selected($filters['date_range'] ?? '', 'this_month'); ?>>This month</option>
4040 </select>
4041
4042 <select name="status" class="sync-filter-select">
4043 <option value="" <?php selected($filters['status'] ?? '', ''); ?>>Status Filter</option>
4044 <option value="published" <?php selected($filters['status'] ?? '', 'published'); ?>>Published</option>
4045 <option value="draft" <?php selected($filters['status'] ?? '', 'draft'); ?>>Draft</option>
4046 </select>
4047 </form>
4048 </div>
4049 </div>
4050
4051 <!-- Sync History List -->
4052 <div class="sync-log-list">
4053 <?php if (empty($sync_records)): ?>
4054 <div class="sync-log-empty">
4055 <div class="sync-log-empty-icon"><span class="dashicons dashicons-media-default" style="font-size:48px;width:48px;height:48px;color:var(--dashboard-text-secondary);"></span></div>
4056 <h3>No sync records found</h3>
4057 <p>Sync records will appear here when content/pages receive new updates.</p>
4058 </div>
4059 <?php else: ?>
4060 <?php foreach ($sync_records as $record): ?>
4061 <div class="sync-log-item">
4062 <div class="sync-log-icon">
4063 <div class="sync-icon-circle">
4064 <span class="sync-icon"><?php echo ($record->source === 'MCP Client') ? '<span class="dashicons dashicons-admin-users" style="font-size:16px;width:16px;height:16px;"></span>' : '<span class="dashicons dashicons-media-default" style="font-size:16px;width:16px;height:16px;"></span>'; ?></span>
4065 </div>
4066 </div>
4067
4068 <div class="sync-log-content">
4069 <div class="sync-log-title"><?php echo esc_html($record->title); ?>
4070 <?php if (!empty($record->url)): ?>
4071 <a href="<?php echo esc_url($record->url); ?>" target="_blank" rel="noopener" title="Open URL" style="margin-left:8px; text-decoration:none;"><span class="dashicons dashicons-external" style="font-size:14px;width:14px;height:14px;vertical-align:middle;"></span></a>
4072 <?php endif; ?>
4073 </div>
4074 <div class="sync-log-meta">
4075 <?php echo esc_html( $this->time_elapsed_string($record->created_at) ); ?>
4076 <?php if (!empty($record->source)): ?>
4077 &nbsp;·&nbsp;<span style="opacity:.7;"><?php echo esc_html($record->source); ?></span>
4078 <?php endif; ?>
4079 </div>
4080 </div>
4081
4082 <div class="sync-log-status" style="display:flex;align-items:center;gap:8px;">
4083 <?php
4084 $st = (string) $record->status;
4085 $b_label = ucfirst($st); $b_bg = '#6b7280'; $b_icon = 'dashicons-info-outline';
4086 if ($st === 'published' || $st === 'publish' || $st === 'success' || $st === 'partial') { $b_label = 'Published'; $b_bg = '#16a34a'; $b_icon = 'dashicons-yes'; }
4087 elseif ($st === 'updated') { $b_label = 'Updated'; $b_bg = '#0d9488'; $b_icon = 'dashicons-update'; }
4088 elseif ($st === 'failed' || $st === 'conflict' || $st === 'locked') { $b_label = 'Not imported'; $b_bg = '#64748b'; $b_icon = 'dashicons-minus'; }
4089 elseif ($st === 'draft') { $b_label = 'Draft'; $b_bg = '#6b7280'; $b_icon = 'dashicons-info-outline'; }
4090 ?>
4091 <span class="sync-status-badge" style="display:inline-flex;align-items:center;gap:4px;background:<?php echo esc_attr($b_bg); ?>;color:#fff;padding:3px 10px;border-radius:12px;font-size:12px;font-weight:600;line-height:1;">
4092 <span class="dashicons <?php echo esc_attr($b_icon); ?>" style="font-size:14px;width:14px;height:14px;line-height:14px;"></span>
4093 <?php echo esc_html($b_label); ?>
4094 </span>
4095 <?php if ($record->source === 'MCP Client'): ?>
4096 <button type="button"
4097 class="metasync-rollback-btn"
4098 data-id="<?php echo esc_attr($record->id); ?>"
4099 data-nonce="<?php echo esc_attr(wp_create_nonce('metasync_rollback_mcp_change')); ?>"
4100 style="background:none;border:1px solid #aaa;border-radius:4px;padding:3px 8px;cursor:pointer;font-size:12px;color:inherit;"
4101 title="Rollback this MCP change">
4102 Rollback
4103 </button>
4104 <?php endif; ?>
4105 </div>
4106 </div>
4107 <?php endforeach; ?>
4108 <?php endif; ?>
4109 </div>
4110
4111 <!-- Pagination -->
4112 <div class="sync-log-pagination">
4113 <div class="sync-log-pagination-info">
4114 Total records: <?php echo intval( $total_records ); ?><?php if ($total_records > 0): ?> | Showing <?php echo intval( $offset ) + 1; ?>-<?php echo intval( min($offset + $per_page, $total_records) ); ?><?php endif; ?>
4115 <?php echo Metasync_Per_Page_Helper::render_selector('sync_log', $per_page); ?>
4116 </div>
4117
4118 <?php if ($total_pages > 1): ?>
4119 <div class="sync-log-pagination-controls">
4120 <?php if ($page > 1): ?>
4121 <a href="?page=<?php echo esc_attr($_GET['page']); ?>&paged=<?php echo intval( $page ) - 1; ?><?php echo esc_html( $this->build_filter_query_string($filters) ); ?>" class="sync-pagination-btn"></a>
4122 <?php endif; ?>
4123
4124 <?php for ($i = max(1, $page - 2); $i <= min($total_pages, $page + 2); $i++): ?>
4125 <a href="?page=<?php echo esc_attr($_GET['page']); ?>&paged=<?php echo intval( $i ); ?><?php echo esc_html( $this->build_filter_query_string($filters) ); ?>"
4126 class="sync-pagination-btn <?php echo $i === $page ? 'active' : ''; ?>"><?php echo intval( $i ); ?></a>
4127 <?php endfor; ?>
4128
4129 <?php if ($page < $total_pages): ?>
4130 <a href="?page=<?php echo esc_attr($_GET['page']); ?>&paged=<?php echo intval( $page ) + 1; ?><?php echo esc_html( $this->build_filter_query_string($filters) ); ?>" class="sync-pagination-btn"></a>
4131 <?php endif; ?>
4132 </div>
4133 <?php endif; ?>
4134 </div>
4135 </div>
4136 <?php $this->render_layout_close(); ?>
4137
4138 <script>
4139 (function () {
4140 // ── Clear Log ──────────────────────────────────────────────
4141 var clearBtn = document.getElementById('metasync-clear-sync-log-btn');
4142 if (clearBtn) {
4143 clearBtn.addEventListener('click', function () {
4144 if (!confirm('Are you sure you want to permanently delete all sync log records? This cannot be undone.')) {
4145 return;
4146 }
4147 clearBtn.disabled = true;
4148 clearBtn.textContent = 'Clearing…';
4149 var data = new FormData();
4150 data.append('action', 'metasync_clear_sync_log');
4151 data.append('nonce', clearBtn.dataset.nonce);
4152 fetch(ajaxurl, { method: 'POST', body: data })
4153 .then(function (r) { return r.json(); })
4154 .then(function (resp) {
4155 if (resp.success) {
4156 window.location.reload();
4157 } else {
4158 alert(resp.data && resp.data.message ? resp.data.message : 'Failed to clear log.');
4159 clearBtn.disabled = false;
4160 clearBtn.textContent = '🗑 Clear Log';
4161 }
4162 })
4163 .catch(function () {
4164 alert('Request failed. Please try again.');
4165 clearBtn.disabled = false;
4166 clearBtn.textContent = '🗑 Clear Log';
4167 });
4168 });
4169 }
4170
4171 // ── Rollback ───────────────────────────────────────────────
4172 document.querySelectorAll('.metasync-rollback-btn').forEach(function (btn) {
4173 btn.addEventListener('click', function () {
4174 if (!confirm('Rollback this MCP change to its previous state?')) {
4175 return;
4176 }
4177 btn.disabled = true;
4178 btn.textContent = '';
4179 var data = new FormData();
4180 data.append('action', 'metasync_rollback_mcp_change');
4181 data.append('nonce', btn.dataset.nonce);
4182 data.append('sync_history_id', btn.dataset.id);
4183 fetch(ajaxurl, { method: 'POST', body: data })
4184 .then(function (r) { return r.json(); })
4185 .then(function (resp) {
4186 if (resp.success) {
4187 btn.textContent = '✓ Done';
4188 btn.style.color = 'green';
4189 } else {
4190 alert(resp.data && resp.data.message ? resp.data.message : 'Rollback failed.');
4191 btn.disabled = false;
4192 btn.textContent = '↩ Rollback';
4193 }
4194 })
4195 .catch(function () {
4196 alert('Request failed. Please try again.');
4197 btn.disabled = false;
4198 btn.textContent = '↩ Rollback';
4199 });
4200 });
4201 });
4202 })();
4203 </script>
4204 <?php
4205 }
4206 /**
4207 * Build filter query string for pagination
4208 */
4209 private function build_filter_query_string($filters)
4210 {
4211 $query_parts = [];
4212 foreach ($filters as $key => $value) {
4213 if (!empty($value)) {
4214 $query_parts[] = $key . '=' . urlencode($value);
4215 }
4216 }
4217
4218 // Preserve the user-selected results-per-page value across page
4219 // navigation so a non-default page size survives clicking a page link.
4220 $per_page_key = Metasync_Per_Page_Helper::request_key('sync_log');
4221 if (isset($_GET[$per_page_key])) {
4222 $per_page = (int) $_GET[$per_page_key];
4223 if (in_array($per_page, Metasync_Per_Page_Helper::allowed_values(), true)) {
4224 $query_parts[] = $per_page_key . '=' . $per_page;
4225 }
4226 }
4227
4228 return !empty($query_parts) ? '&' . implode('&', $query_parts) : '';
4229 }
4230
4231 /**
4232 * Handle AJAX requests for sync log data
4233 */
4234 private function handle_sync_log_ajax()
4235 {
4236 // This can be used for future AJAX functionality like real-time updates
4237 wp_die();
4238 }
4239
4240 /**
4241 * AJAX: Clear all Sync Log records (admin-only, nonce protected).
4242 */
4243 public function ajax_clear_sync_log()
4244 {
4245 check_ajax_referer('metasync_clear_sync_log', 'nonce');
4246
4247 if (!current_user_can('manage_options')) {
4248 wp_send_json_error(['message' => 'Insufficient permissions.'], 403);
4249 }
4250
4251 $sync_db = new Metasync_Sync_History_Database();
4252 $sync_db->clear_logs();
4253
4254 wp_send_json_success(['message' => 'Sync log cleared successfully.']);
4255 }
4256
4257 /**
4258 * AJAX: Rollback a single MCP Client sync history entry.
4259 */
4260 public function ajax_rollback_mcp_change()
4261 {
4262 check_ajax_referer('metasync_rollback_mcp_change', 'nonce');
4263
4264 if (!current_user_can('manage_options')) {
4265 wp_send_json_error(['message' => 'Insufficient permissions.'], 403);
4266 }
4267
4268 $id = isset($_POST['sync_history_id']) ? intval($_POST['sync_history_id']) : 0;
4269 if (!$id) {
4270 wp_send_json_error(['message' => 'Invalid sync history ID.']);
4271 }
4272
4273 $result = Metasync_MCP_Sync_Logger::rollback($id);
4274
4275 if ($result['success']) {
4276 wp_send_json_success(['message' => $result['message']]);
4277 } else {
4278 wp_send_json_error(['message' => $result['message']]);
4279 }
4280 }
4281
4282 /**
4283 * Render compatibility sections
4284 */
4285 private function render_compatibility_sections()
4286 {
4287 Metasync_Compatibility_Checker::instance()->render_compatibility_sections();
4288 }
4289
4290 /**
4291 * Render Page Builders section
4292 */
4293 private function render_page_builders_section()
4294 {
4295 Metasync_Compatibility_Checker::instance()->render_page_builders_section();
4296 }
4297
4298 /**
4299 * Render SEO Plugins section
4300 */
4301 private function render_seo_plugins_section()
4302 {
4303 Metasync_Compatibility_Checker::instance()->render_seo_plugins_section();
4304 }
4305
4306 /**
4307 * Render Cache Plugins section
4308 */
4309 private function render_cache_plugins_section()
4310 {
4311 Metasync_Compatibility_Checker::instance()->render_cache_plugins_section();
4312 }
4313
4314 /**
4315 * Render Lock Section button for protected tabs
4316 *
4317 * @param string $tab The tab identifier (general, whitelabel, advanced)
4318 */
4319 private function render_lock_button($tab)
4320 {
4321 Metasync_Compatibility_Checker::instance()->render_lock_button($tab);
4322 }
4323
4324 /**
4325 * Get Page Builders compatibility information
4326 */
4327 private function get_page_builders_compatibility()
4328 {
4329 return Metasync_Compatibility_Checker::instance()->get_page_builders_compatibility();
4330 }
4331
4332 /**
4333 * Get SEO Plugins compatibility information
4334 */
4335 private function get_seo_plugins_compatibility()
4336 {
4337 return Metasync_Compatibility_Checker::instance()->get_seo_plugins_compatibility();
4338 }
4339
4340 /**
4341 * Get Cache Plugins compatibility information
4342 */
4343 private function get_cache_plugins_compatibility()
4344 {
4345 return Metasync_Compatibility_Checker::instance()->get_cache_plugins_compatibility();
4346 }
4347
4348 /**
4349 * Check if a plugin is installed and active
4350 * @deprecated Use get_plugin_status() instead
4351 */
4352 private function is_plugin_installed($plugin_file)
4353 {
4354 return Metasync_Compatibility_Checker::instance()->is_plugin_installed($plugin_file);
4355 }
4356
4357 /**
4358 * Get detailed plugin status (installed and/or active)
4359 * Checks multiple plugin file paths (e.g., free and premium versions)
4360 *
4361 * @param array $plugin_files Array of plugin file paths to check (e.g., ['free/plugin.php', 'pro/plugin.php'])
4362 * @param bool $is_core Whether this is a WordPress core feature (always installed/active)
4363 * @param string $theme_name Optional theme name to check if it's a theme instead of plugin
4364 * @return array ['is_installed' => bool, 'is_active' => bool, 'active_version' => string|null]
4365 */
4366 private function get_plugin_status($plugin_files, $is_core = false, $theme_name = null)
4367 {
4368 return Metasync_Compatibility_Checker::instance()->get_plugin_status($plugin_files, $is_core, $theme_name);
4369 }
4370
4371
4372 /**
4373 * Get plugin logo URL (optimized for performance)
4374 */
4375 private function get_plugin_logo($plugin_key, $type)
4376 {
4377 return Metasync_Compatibility_Checker::instance()->get_plugin_logo($plugin_key, $type);
4378 }
4379
4380
4381 public function creat_error_Logs_List()
4382 {
4383 Metasync_Admin_Pages::get_instance($this)->creat_error_Logs_List();
4384 }
4385
4386 /**
4387 * Site error logs page callback
4388 */
4389 public function create_admin_heartbeat_error_logs_page()
4390 {
4391 Metasync_Admin_Pages::get_instance($this)->create_admin_heartbeat_error_logs_page();
4392 }
4393
4394
4395 /**
4396 * Handle session management early for whitelabel functionality
4397 */
4398 private function handle_session_management_early()
4399 {
4400 Metasync_Connect_Manager::instance()->handle_session_management_early();
4401 }
4402
4403 /**
4404 * @deprecated 2.5.12 Use Metasync_Auth_Manager instead of sessions for authentication
4405 */
4406 private function safe_session_start() {
4407 // This method is deprecated and no longer used
4408 // Authentication now uses Metasync_Auth_Manager with WordPress transients and user meta
4409 _deprecated_function(__METHOD__, '2.5.12', 'Metasync_Auth_Manager');
4410 return Metasync_Session_Helper::safe_start();
4411 }
4412
4413 private function handle_whitelabel_session_logic()
4414 {
4415 Metasync_Connect_Manager::instance()->handle_whitelabel_session_logic();
4416 }
4417
4418 private function handle_whitelabel_password_early()
4419 {
4420 Metasync_Connect_Manager::instance()->handle_whitelabel_password_early();
4421 }
4422
4423 /**
4424 * Get accordion sections configuration for General Settings
4425 *
4426 * @return array Accordion sections with field IDs, icons, and descriptions
4427 */
4428 private function get_accordion_sections_config() {
4429 return Metasync_Settings_Fields::instance()->get_accordion_sections_config();
4430 }
4431
4432 /**
4433 * Get accordion sections configuration for Advanced Settings Tab
4434 *
4435 * @return array Accordion sections configuration
4436 */
4437 private function get_advanced_accordion_config() {
4438 return Metasync_Settings_Fields::instance()->get_advanced_accordion_config();
4439 }
4440
4441 /**
4442 * Render accordion sections for Advanced Settings Tab
4443 */
4444 public function render_advanced_accordion() {
4445 Metasync_Settings_Fields::instance()->render_advanced_accordion();
4446 }
4447
4448 /**
4449 * Render reset settings section for Advanced tab accordion
4450 */
4451 /**
4452 * Render CPU Monitor section for Performance accordion
4453 */
4454 public function render_cpu_monitor_section() {
4455 $cpu_monitor = new Metasync_CPU_Monitor();
4456 $stats = Metasync_CPU_Monitor::get_stats();
4457 $per_core_threshold = Metasync_CPU_Monitor::get_per_core_threshold();
4458 $cores = Metasync_CPU_Monitor::get_cpu_core_count();
4459 $effective_threshold = Metasync_CPU_Monitor::get_effective_threshold();
4460 $detection_reliable = Metasync_CPU_Monitor::is_core_detection_reliable();
4461 ?>
4462 <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px;">
4463 <!-- CPU Cores Detected -->
4464 <div style="margin-bottom: 24px;">
4465 <label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
4466 CPU Cores Detected
4467 </label>
4468 <div style="padding: 10px 12px; background: var(--dashboard-input-bg); border: 1px solid var(--dashboard-border); border-radius: 6px; color: var(--dashboard-text-secondary);">
4469 <?php if ($detection_reliable) : ?>
4470 <strong><?php echo intval($cores); ?></strong> core<?php echo $cores !== 1 ? 's' : ''; ?>
4471 <?php else : ?>
4472 <strong>Not detected</strong>
4473 <?php endif; ?>
4474 </div>
4475 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
4476 <?php if ($detection_reliable) : ?>
4477 Automatically detected on this system.
4478 <?php else : ?>
4479 Core detection is not available on this hosting environment.
4480 <?php endif; ?>
4481 </p>
4482 </div>
4483
4484 <!-- Per-Core Load Threshold -->
4485 <div style="margin-bottom: 24px;">
4486 <label for="cpu_load_per_core_threshold" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
4487 Per-Core Load Threshold
4488 <?php Metasync::render_tooltip_icon('cpu_per_core_load_threshold', 'How busy each CPU core can get before the plugin pauses background SEO syncing. Lower = more cautious. Most sites leave this at 2.0.'); ?>
4489 </label>
4490 <input type="number"
4491 id="cpu_load_per_core_threshold"
4492 name="metasync_options[performance][cpu_load_per_core_threshold]"
4493 value="<?php echo esc_attr($per_core_threshold); ?>"
4494 step="0.1"
4495 min="0.5"
4496 max="10.0"
4497 style="width: 100%; padding: 10px 12px; background: var(--dashboard-input-bg); border: 1px solid var(--dashboard-border); border-radius: 6px; color: var(--dashboard-text); font-size: 14px; box-sizing: border-box;"
4498 onchange="updateEffectiveThreshold()">
4499 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
4500 Set the load average per CPU core (0.5–10.0). Default: 2.0
4501 </p>
4502 </div>
4503
4504 <!-- Effective Threshold (Read-Only) -->
4505 <div style="margin-bottom: 24px;">
4506 <label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
4507 Effective Threshold
4508 </label>
4509 <div style="padding: 10px 12px; background: var(--dashboard-input-bg); border: 1px solid var(--dashboard-border); border-radius: 6px; color: var(--dashboard-text-secondary);">
4510 <strong id="effective_threshold_value"><?php echo round($effective_threshold, 2); ?></strong>
4511 </div>
4512 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
4513 Calculated as: cores × per-core threshold
4514 </p>
4515 </div>
4516
4517 <!-- Statistics -->
4518 <div style="background: rgba(59, 130, 246, 0.05); border: 1px solid rgba(59, 130, 246, 0.2); border-radius: 8px; padding: 16px; margin-bottom: 24px;">
4519 <h4 style="margin: 0 0 12px 0; color: var(--dashboard-text); display: flex; align-items: center; gap: 8px;">
4520 <span class="dashicons dashicons-chart-bar" style="font-size:18px;width:18px;height:18px;"></span>
4521 <span>CPU Load Statistics</span>
4522 </h4>
4523 <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px;">
4524 <div>
4525 <div style="font-size: 12px; color: var(--dashboard-text-secondary); margin-bottom: 4px;">Total Deferrals</div>
4526 <div style="font-size: 18px; font-weight: 600; color: var(--dashboard-text);"><?php echo intval($stats['deferrals']); ?></div>
4527 </div>
4528 <div>
4529 <div style="font-size: 12px; color: var(--dashboard-text-secondary); margin-bottom: 4px;">Max Load Observed</div>
4530 <div style="font-size: 18px; font-weight: 600; color: var(--dashboard-text);"><?php echo round($stats['max_load'], 2); ?></div>
4531 </div>
4532 <div>
4533 <div style="font-size: 12px; color: var(--dashboard-text-secondary); margin-bottom: 4px;">Average Load</div>
4534 <div style="font-size: 18px; font-weight: 600; color: var(--dashboard-text);"><?php echo round($stats['avg_load'], 2); ?></div>
4535 </div>
4536 </div>
4537 </div>
4538
4539 <!-- Save Button -->
4540 <button type="button" class="metasync-btn-primary" onclick="submitPerformanceSettings(event)" style="background: var(--dashboard-primary, #3b82f6); color: #ffffff; border: none; padding: 12px 24px; border-radius: 8px; font-weight: 500; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);" onmouseover="this.style.background='var(--dashboard-primary-hover, #2563eb)'; this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 8px rgba(59, 130, 246, 0.3)';" onmouseout="this.style.background='var(--dashboard-primary, #3b82f6)'; this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 4px rgba(59, 130, 246, 0.2)';">
4541 Save Performance Settings
4542 </button>
4543 </div>
4544 <script>
4545 function updateEffectiveThreshold() {
4546 const coresCount = <?php echo intval($cores); ?>;
4547 const perCoreInput = document.getElementById('cpu_load_per_core_threshold');
4548 const effectiveValue = coresCount * parseFloat(perCoreInput.value);
4549 document.getElementById('effective_threshold_value').textContent = effectiveValue.toFixed(2);
4550 }
4551
4552 function submitPerformanceSettings(event) {
4553 // Prevent any form submission (defensive)
4554 if (event) {
4555 event.preventDefault();
4556 }
4557
4558 // Get the threshold value
4559 const thresholdInput = document.getElementById('cpu_load_per_core_threshold');
4560 if (!thresholdInput) {
4561 console.error('CPU threshold input not found');
4562 return;
4563 }
4564
4565 const threshold = parseFloat(thresholdInput.value);
4566 if (isNaN(threshold) || threshold < 0.5 || threshold > 10.0) {
4567 alert('Please enter a valid threshold between 0.5 and 10.0');
4568 return;
4569 }
4570
4571 // Get AJAX URL
4572 const ajaxUrl = (typeof window.ajaxurl !== 'undefined' && window.ajaxurl)
4573 ? window.ajaxurl
4574 : '<?php echo esc_js(admin_url('admin-ajax.php')); ?>';
4575
4576 // Get nonce from form
4577 const nonceInput = document.querySelector('input[name="meta_sync_nonce"]');
4578 const nonce = nonceInput ? nonceInput.value : '';
4579
4580 // Prepare AJAX request data
4581 const formData = new FormData();
4582 formData.append('action', 'metasync_save_performance_settings');
4583 formData.append('meta_sync_nonce', nonce);
4584 formData.append('metasync_options[performance][cpu_load_per_core_threshold]', threshold);
4585
4586 // Show saving state
4587 const button = event.target;
4588 const originalText = button.innerHTML;
4589 button.innerHTML = '⏳ Saving...';
4590 button.disabled = true;
4591
4592 // Make AJAX request
4593 fetch(ajaxUrl, {
4594 method: 'POST',
4595 body: formData,
4596 headers: {
4597 'X-Requested-With': 'XMLHttpRequest'
4598 }
4599 })
4600 .then(response => response.json())
4601 .then(data => {
4602 button.innerHTML = originalText;
4603 button.disabled = false;
4604
4605 if (data.success) {
4606 // Update the effective threshold display
4607 if (data.data && data.data.effective_threshold) {
4608 document.getElementById('effective_threshold_value').textContent =
4609 data.data.effective_threshold.toFixed(2);
4610 }
4611
4612 // Show success notice
4613 showPerformanceNotice(data.data.message || 'Settings saved successfully!', 'success');
4614 } else {
4615 // Show error notice
4616 showPerformanceNotice(data.data?.message || 'Failed to save settings', 'error');
4617 }
4618 })
4619 .catch(error => {
4620 console.error('AJAX Error:', error);
4621 button.innerHTML = originalText;
4622 button.disabled = false;
4623 showPerformanceNotice('An error occurred while saving settings', 'error');
4624 });
4625 }
4626
4627 function showPerformanceNotice(message, type) {
4628 // Create notice element
4629 const notice = document.createElement('div');
4630 notice.className = `notice notice-${type} is-dismissible`;
4631 notice.style.cssText = 'margin: 20px auto; max-width: 800px;';
4632 notice.innerHTML = `
4633 <p><strong>${type === 'success' ? '�
4634 ' : '❌'} ${message}</strong></p>
4635 <button type="button" class="notice-dismiss" onclick="this.parentElement.remove()" style="cursor: pointer;"></button>
4636 `;
4637
4638 // Find the page header to insert before
4639 const pageHeader = document.querySelector('h1, h2');
4640 if (pageHeader) {
4641 pageHeader.parentElement.insertBefore(notice, pageHeader.nextSibling);
4642 } else {
4643 document.body.insertBefore(notice, document.body.firstChild);
4644 }
4645
4646 // Auto-remove error notices after 5 seconds
4647 if (type === 'error') {
4648 setTimeout(() => {
4649 if (notice.parentElement) {
4650 notice.remove();
4651 }
4652 }, 5000);
4653 }
4654 }
4655 </script>
4656 <?php
4657 }
4658
4659 private function render_reset_settings_section() {
4660 Metasync_Settings_Fields::instance()->render_reset_settings_section();
4661 }
4662
4663 /**
4664 * Render Google Index API section for Indexation Control page
4665 */
4666 public function render_google_index_section() {
4667 Metasync_Settings_Fields::instance()->render_google_index_section();
4668 }
4669
4670 /**
4671 * Render Bing Index (IndexNow) section
4672 *
4673 * @since 2.6.0
4674 * @return void
4675 */
4676 public function render_bing_index_section() {
4677 Metasync_Settings_Fields::instance()->render_bing_index_section();
4678 }
4679
4680 /**
4681 * Render Plugin Access Roles section for Advanced tab accordion
4682 */
4683 private function render_plugin_access_roles_section() {
4684 Metasync_Settings_Fields::instance()->render_plugin_access_roles_section();
4685 }
4686
4687 /**
4688 * Check if the current user has access to the plugin based on role settings
4689 * Wrapper method that delegates to the common Metasync::current_user_has_plugin_access()
4690 * but also requires manage_options capability for admin area access
4691 *
4692 * @return bool True if user has access, false otherwise
4693 */
4694 public function current_user_has_plugin_access() {
4695 return Metasync::current_user_has_plugin_access();
4696 }
4697
4698 /**
4699 * Get default execution settings
4700 *
4701 * @return array Default execution settings
4702 */
4703 private function get_default_execution_settings() {
4704 return Metasync_Settings_Fields::instance()->get_default_execution_settings();
4705 }
4706
4707 /**
4708 * Get execution setting value
4709 *
4710 * @param string $key Setting key
4711 * @param mixed $default Default value if setting doesn't exist
4712 * @return mixed Setting value or default
4713 */
4714 public function get_execution_setting($key, $default = null) {
4715 return Metasync_Settings_Fields::instance()->get_execution_setting($key, $default);
4716 }
4717
4718 /**
4719 * Get all execution settings
4720 *
4721 * @return array All execution settings with defaults merged
4722 */
4723 public function get_all_execution_settings() {
4724 return Metasync_Settings_Fields::instance()->get_all_execution_settings();
4725 }
4726
4727 /**
4728 * Check if server allows changing memory limit
4729 * Tests if ini_set('memory_limit') is allowed
4730 *
4731 * @return bool True if memory limit can be changed, false otherwise
4732 */
4733 private function can_change_memory_limit() {
4734 return Metasync_Settings_Fields::instance()->can_change_memory_limit();
4735 }
4736
4737 /**
4738 * Get PHP server limits for display
4739 *
4740 * @return array Server limits (execution_time, memory_limit, can_change_memory)
4741 */
4742 private function get_server_limits() {
4743 return Metasync_Settings_Fields::instance()->get_server_limits();
4744 }
4745
4746 /**
4747 * Apply memory limit from execution settings
4748 * Only applies if server allows changing memory limit
4749 *
4750 * @return bool True if memory limit was applied, false otherwise
4751 */
4752 public function apply_memory_limit() {
4753 return Metasync_Settings_Fields::instance()->apply_memory_limit();
4754 }
4755
4756 /**
4757 * Parse memory limit string to MB
4758 *
4759 * @param string $memory_limit Memory limit string (e.g., "256M", "1G")
4760 * @return int Memory limit in MB
4761 */
4762 private function parse_memory_limit_to_mb($memory_limit) {
4763 return Metasync_Settings_Fields::instance()->parse_memory_limit_to_mb($memory_limit);
4764 }
4765
4766 /**
4767 * Render Execution Settings section for Advanced tab accordion
4768 */
4769 private function render_execution_settings_section() {
4770 Metasync_Settings_Fields::instance()->render_execution_settings_section();
4771 }
4772
4773 /**
4774 * Get tooltip content for settings fields
4775 *
4776 * @return array Field ID => Tooltip text mapping
4777 */
4778 private function get_field_tooltips() {
4779 return Metasync_Settings_Fields::instance()->get_field_tooltips();
4780 }
4781
4782 /**
4783 * Get the section key for a given field ID
4784 *
4785 * @param string $field_id The settings field ID
4786 * @return string|null Section key or null if not found
4787 */
4788 private function get_field_section($field_id) {
4789 return Metasync_Settings_Fields::instance()->get_field_section($field_id);
4790 }
4791
4792 /**
4793 * Render accordion sections for General Settings
4794 *
4795 * @param string $page The settings page slug
4796 */
4797 public function render_accordion_sections($page) {
4798 Metasync_Settings_Fields::instance()->render_accordion_sections($page);
4799 }
4800
4801 /**
4802 * Register and add settings
4803 */
4804 public function settings_page_init()
4805 {
4806 Metasync_Settings_Registration::instance()->settings_page_init();
4807 }
4808
4809 /**
4810 * Sanitize each setting field as needed
4811 *
4812 * @param array $input Contains all settings fields as array keys
4813 */
4814 public function sanitize($input)
4815 {
4816 return Metasync_Settings_Registration::instance()->sanitize($input);
4817 }
4818
4819 public function metasync_settings_genkey_callback()
4820 {
4821 Metasync_Settings_Fields::instance()->metasync_settings_genkey_callback();
4822 }
4823
4824 /**
4825 * Get the settings option array and print one of its values
4826 */
4827 public function linkgraph_token_callback()
4828 {
4829 Metasync_Settings_Fields::instance()->linkgraph_token_callback();
4830 }
4831
4832
4833 private function time_elapsed_string($datetime, $full = false)
4834 {
4835 return Metasync_Settings_Fields::instance()->time_elapsed_string($datetime, $full);
4836 }
4837
4838 /**
4839 * Get the settings option array and print one of its values
4840 */
4841 public function searchatlas_api_key_callback()
4842 {
4843 Metasync_Settings_Fields::instance()->searchatlas_api_key_callback();
4844 }
4845
4846
4847 /**
4848 * Site Verification Tools
4849 *
4850 * Bing Site Verification
4851 * Baidu Site Verification
4852 * Alexa Site Verification
4853 * Yandex Site Verification
4854 * Google Site Verification
4855 * Pinterest Site Verification
4856 * Norton Safe Web Site Verification
4857 */
4858
4859 /**
4860 * Get the settings option array and print one of its values
4861 */
4862 public function bing_site_verification_callback()
4863 {
4864 Metasync_Settings_Fields::instance()->bing_site_verification_callback();
4865 }
4866
4867
4868
4869
4870
4871 /**
4872 * Get the settings option array and print one of its values
4873 */
4874 public function yandex_site_verification_callback()
4875 {
4876 Metasync_Settings_Fields::instance()->yandex_site_verification_callback();
4877 }
4878
4879 /**
4880 * Get the settings option array and print one of its values
4881 */
4882 public function google_site_verification_callback()
4883 {
4884 Metasync_Settings_Fields::instance()->google_site_verification_callback();
4885 }
4886
4887 /**
4888 * Get the settings option array and print one of its values
4889 */
4890 public function pinterest_site_verification_callback()
4891 {
4892 Metasync_Settings_Fields::instance()->pinterest_site_verification_callback();
4893 }
4894
4895
4896
4897 /**
4898 * Local SEO for business and person
4899 *
4900 */
4901
4902 /**
4903 * Get the settings option array and print one of its values
4904 */
4905 public function local_seo_person_organization_callback()
4906 {
4907 Metasync_Settings_Fields::instance()->local_seo_person_organization_callback();
4908 }
4909
4910 /**
4911 * Get the settings option array and print one of its values
4912 */
4913 public function local_seo_name_callback()
4914 {
4915 Metasync_Settings_Fields::instance()->local_seo_name_callback();
4916 }
4917
4918 /**
4919 * Get the settings option array and print one of its values
4920 */
4921 public function local_seo_logo_callback()
4922 {
4923 Metasync_Settings_Fields::instance()->local_seo_logo_callback();
4924 }
4925
4926 /**
4927 * Get the settings option array and print one of its values
4928 */
4929 public function local_seo_url_callback()
4930 {
4931 Metasync_Settings_Fields::instance()->local_seo_url_callback();
4932 }
4933
4934 /**
4935 * Get the settings option array and print one of its values
4936 */
4937 public function local_seo_email_callback()
4938 {
4939 Metasync_Settings_Fields::instance()->local_seo_email_callback();
4940 }
4941
4942 /**
4943 * Get the settings option array and print one of its values
4944 */
4945 public function local_seo_phone_callback()
4946 {
4947 Metasync_Settings_Fields::instance()->local_seo_phone_callback();
4948 }
4949
4950 /**
4951 * Get the settings option array and print one of its values
4952 */
4953 public function local_seo_address_callback()
4954 {
4955 Metasync_Settings_Fields::instance()->local_seo_address_callback();
4956 }
4957
4958 /**
4959 * Get the settings option array and print one of its values
4960 */
4961 public function local_seo_business_type_callback()
4962 {
4963 Metasync_Settings_Fields::instance()->local_seo_business_type_callback();
4964 }
4965
4966
4967
4968 /**
4969 * Get the settings option array and print one of its values
4970 */
4971 public function local_seo_opening_hours_callback()
4972 {
4973 Metasync_Settings_Fields::instance()->local_seo_opening_hours_callback();
4974 }
4975
4976 /**
4977 * Get the settings option array and print one of its values
4978 */
4979 public function local_seo_phone_numbers_callback()
4980 {
4981 Metasync_Settings_Fields::instance()->local_seo_phone_numbers_callback();
4982 }
4983
4984 /**
4985 * Get the settings option array and print one of its values
4986 */
4987 public function local_seo_price_range_callback()
4988 {
4989 Metasync_Settings_Fields::instance()->local_seo_price_range_callback();
4990 }
4991
4992 /**
4993 * Get the settings option array and print one of its values
4994 */
4995 public function local_seo_about_page_callback()
4996 {
4997 Metasync_Settings_Fields::instance()->local_seo_about_page_callback();
4998 }
4999
5000 /**
5001 * Get the settings option array and print one of its values
5002 */
5003 public function local_seo_contact_page_callback()
5004 {
5005 Metasync_Settings_Fields::instance()->local_seo_contact_page_callback();
5006 }
5007
5008 /**
5009 * Get the settings option array and print one of its values
5010 */
5011 public function local_seo_map_key_callback()
5012 {
5013 Metasync_Settings_Fields::instance()->local_seo_map_key_callback();
5014 }
5015
5016 /**
5017 * Get the settings option array and print one of its values
5018 */
5019 public function local_seo_geo_coordinates_callback()
5020 {
5021 Metasync_Settings_Fields::instance()->local_seo_geo_coordinates_callback();
5022 }
5023
5024 /**
5025 * Get the settings option array and print one of its values
5026 */
5027 public function header_snippets_callback()
5028 {
5029 Metasync_Settings_Fields::instance()->header_snippets_callback();
5030 }
5031
5032 /**
5033 * Get the settings option array and print one of its values
5034 */
5035 public function footer_snippets_callback()
5036 {
5037 Metasync_Settings_Fields::instance()->footer_snippets_callback();
5038 }
5039
5040 /**
5041 * Get the settings option array and print one of its values
5042 */
5043 public function common_robot_meta_tags_callback()
5044 {
5045 Metasync_Settings_Fields::instance()->common_robot_meta_tags_callback();
5046 }
5047
5048 /**
5049 * Backward compatibility alias for common_robot_mata_tags_callback
5050 * @deprecated Use common_robot_meta_tags_callback() instead
5051 */
5052 public function common_robot_mata_tags_callback()
5053 {
5054 Metasync_Settings_Fields::instance()->common_robot_mata_tags_callback();
5055 }
5056
5057 /**
5058 * Get the settings option array and print one of its values
5059 */
5060 public function advance_robot_meta_tags_callback()
5061 {
5062 Metasync_Settings_Fields::instance()->advance_robot_meta_tags_callback();
5063 }
5064
5065 /**
5066 * Backward compatibility alias for advance_robot_mata_tags_callback
5067 * @deprecated Use advance_robot_meta_tags_callback() instead
5068 */
5069 public function advance_robot_mata_tags_callback()
5070 {
5071 Metasync_Settings_Fields::instance()->advance_robot_mata_tags_callback();
5072 }
5073
5074 /**
5075 * Get the settings option array and print one of its values
5076 */
5077 public function global_twitter_card_type_callback()
5078 {
5079 Metasync_Settings_Fields::instance()->global_twitter_card_type_callback();
5080 }
5081
5082 /**
5083 * Get the settings option array and print one of its values
5084 */
5085 public function global_open_graph_meta_callback()
5086 {
5087 Metasync_Settings_Fields::instance()->global_open_graph_meta_callback();
5088 }
5089
5090 /**
5091 * Get the settings option array and print one of its values
5092 */
5093 public function global_facebook_meta_callback()
5094 {
5095 Metasync_Settings_Fields::instance()->global_facebook_meta_callback();
5096 }
5097
5098 /**
5099 * Get the settings option array and print one of its values
5100 */
5101 public function global_twitter_meta_callback()
5102 {
5103 Metasync_Settings_Fields::instance()->global_twitter_meta_callback();
5104 }
5105
5106 /**
5107 * Get the settings option array and print one of its values
5108 */
5109 public function og_image_dimensions_callback()
5110 {
5111 Metasync_Settings_Fields::instance()->og_image_dimensions_callback();
5112 }
5113
5114 /**
5115 * Get the settings option array and print one of its values
5116 */
5117 public function article_timestamps_callback()
5118 {
5119 Metasync_Settings_Fields::instance()->article_timestamps_callback();
5120 }
5121
5122 /**
5123 * Get the settings option array and print one of its values
5124 */
5125 public function article_author_callback()
5126 {
5127 Metasync_Settings_Fields::instance()->article_author_callback();
5128 }
5129
5130 /**
5131 * Get the settings option array and print one of its values
5132 */
5133 public function article_section_callback()
5134 {
5135 Metasync_Settings_Fields::instance()->article_section_callback();
5136 }
5137
5138 /**
5139 * Get the settings option array and print one of its values
5140 */
5141 public function article_tags_callback()
5142 {
5143 Metasync_Settings_Fields::instance()->article_tags_callback();
5144 }
5145
5146 /**
5147 * Get the settings option array and print one of its values
5148 */
5149 public function twitter_image_alt_callback()
5150 {
5151 Metasync_Settings_Fields::instance()->twitter_image_alt_callback();
5152 }
5153
5154 /**
5155 * Get the settings option array and print one of its values
5156 */
5157 public function facebook_page_url_callback()
5158 {
5159 Metasync_Settings_Fields::instance()->facebook_page_url_callback();
5160 }
5161
5162 /**
5163 * Get the settings option array and print one of its values
5164 */
5165 public function facebook_authorship_callback()
5166 {
5167 Metasync_Settings_Fields::instance()->facebook_authorship_callback();
5168 }
5169
5170 /**
5171 * Get the settings option array and print one of its values
5172 */
5173 public function facebook_admin_callback()
5174 {
5175 Metasync_Settings_Fields::instance()->facebook_admin_callback();
5176 }
5177
5178 /**
5179 * Get the settings option array and print one of its values
5180 */
5181 public function facebook_app_callback()
5182 {
5183 Metasync_Settings_Fields::instance()->facebook_app_callback();
5184 }
5185
5186 /**
5187 * Get the settings option array and print one of its values
5188 */
5189 public function facebook_secret_callback()
5190 {
5191 Metasync_Settings_Fields::instance()->facebook_secret_callback();
5192 }
5193
5194 /**
5195 * Get the settings option array and print one of its values
5196 */
5197 public function twitter_username_callback()
5198 {
5199 Metasync_Settings_Fields::instance()->twitter_username_callback();
5200 }
5201
5202 /**
5203 * Get business types as choices in local business.
5204 *
5205 * @return array
5206 */
5207 public static function get_business_types()
5208 {
5209 return Metasync_Settings_Fields::get_business_types();
5210 }
5211
5212 /**
5213 * Display a dashboard warning when using the plain permalink structure.
5214 * @param $data An array of data passed.
5215 */
5216 public function permalink_structure_dashboard_warning() {
5217 $current_permalink_structure = get_option('permalink_structure');
5218
5219 # Get the plugin name using centralized method
5220 $plugin_name = Metasync::get_effective_plugin_name();
5221
5222 # Check if the current permalink structure is set to "Plain"
5223 if ($current_permalink_structure === '/%post_id%/' || $current_permalink_structure === '') {
5224 printf(
5225 '<div class="notice notice-error is-dismissible">
5226 <p>
5227 <b>Warning from %s</b><br>
5228 To ensure compatibility, please update your permalink structure to any option other than "Plain".
5229 For any inquiries, contact support.
5230 </p>
5231 </div>',
5232 esc_html($plugin_name)
5233 );
5234 }
5235 }
5236
5237 /**
5238 * Show a one-time admin notice when a page builder is detected but the
5239 * "Default Page Builder" setting has never been explicitly saved.
5240 */
5241 public function display_page_builder_notice() {
5242 // Only relevant on the plugin's own settings page — avoid repeating
5243 // this notice across every admin screen.
5244 if (!isset($_GET['page']) || $_GET['page'] !== self::$page_slug) {
5245 return;
5246 }
5247
5248 $configured = Metasync::get_option('general')['default_page_builder'] ?? '';
5249
5250 // Setting already saved — nothing to warn about
5251 if (!empty($configured)) {
5252 return;
5253 }
5254
5255 // Check if user dismissed this notice
5256 $dismissed = get_user_meta(get_current_user_id(), 'metasync_builder_notice_dismissed', true);
5257 if ($dismissed) {
5258 return;
5259 }
5260
5261 // Handle dismiss action
5262 if (isset($_GET['metasync_dismiss_builder_notice']) && wp_verify_nonce($_GET['_wpnonce'] ?? '', 'metasync_dismiss_builder')) {
5263 update_user_meta(get_current_user_id(), 'metasync_builder_notice_dismissed', '1');
5264 return;
5265 }
5266
5267 require_once plugin_dir_path(dirname(__FILE__)) . 'custom-pages/class-metasync-html-to-builder-converter.php';
5268 $detected = Metasync_HTML_To_Builder_Converter::auto_detect_builder();
5269
5270 // No non-Gutenberg builder detected — no need to warn
5271 if ($detected === 'gutenberg') {
5272 return;
5273 }
5274
5275 $builders = Metasync_HTML_To_Builder_Converter::get_available_builders();
5276 $builder_label = $builders[$detected]['label'] ?? $detected;
5277 $plugin_name = Metasync::get_effective_plugin_name();
5278 $settings_url = admin_url('admin.php?page=' . self::$page_slug . '&tab=general#metasync-section-content_rendering');
5279 $dismiss_url = wp_nonce_url(add_query_arg('metasync_dismiss_builder_notice', '1'), 'metasync_dismiss_builder');
5280
5281 printf(
5282 '<div class="notice notice-info is-dismissible" style="border-left-color: #0073aa;">
5283 <p>
5284 <strong>%s — Page Builder Detected</strong><br>
5285 <strong>%s</strong> is active on this site. Content synced by Content Genius currently uses <strong>Gutenberg (WordPress Block Editor)</strong> format by default.
5286 </p>
5287 <p>
5288 If you want synced content to use %s\'s native widget format instead, you can change this in
5289 <a href="%s"><strong>Settings → Content Rendering → Default Page Builder</strong></a>.
5290 </p>
5291 <p><a href="%s" style="text-decoration: none;">Dismiss this notice</a></p>
5292 </div>',
5293 esc_html($plugin_name),
5294 esc_html($builder_label),
5295 esc_html($builder_label),
5296 esc_url($settings_url),
5297 esc_url($dismiss_url)
5298 );
5299 }
5300
5301 /**
5302 * Display update warning banner if plugin update is available
5303 * Checks WordPress update API to see if a newer version is available
5304 *
5305 * @since 1.0.0
5306 */
5307 public function display_update_warning_banner() {
5308 // Get the installed version from database
5309 $installed_version = get_option('metasync_version', '0.0.0');
5310
5311 // Get plugin basename for WordPress update API check
5312 // This is the plugin file path relative to plugins directory (e.g., 'metasync/metasync.php')
5313 $plugin_file = plugin_basename(plugin_dir_path(dirname(__FILE__)) . 'metasync.php');
5314
5315 // Get WordPress update plugins transient (contains available updates)
5316 $update_plugins = get_site_transient('update_plugins');
5317
5318 // Check if update information exists and if our plugin has an update available
5319 if ($update_plugins && isset($update_plugins->response) && isset($update_plugins->response[$plugin_file])) {
5320 $update_info = $update_plugins->response[$plugin_file];
5321 $latest_version = isset($update_info->new_version) ? $update_info->new_version : '';
5322
5323 // Compare installed version with latest available version
5324 if ($latest_version && version_compare($installed_version, $latest_version, '<')) {
5325 // Get the plugin name using centralized method
5326 $plugin_name = Metasync::get_effective_plugin_name();
5327
5328 // Show admin notice with plugin name included in the message
5329 printf(
5330 '<div class="notice notice-error is-dismissible">
5331 <p>
5332 <b>Warning from %s</b><br>
5333 A new version of %s is available. Please update to the latest version to ensure compatibility and access new features.
5334 For any inquiries, contact support.
5335 </p>
5336 </div>',
5337 esc_html($plugin_name),
5338 esc_html($plugin_name)
5339 );
5340 }
5341 }
5342 }
5343
5344
5345 /*
5346 Method to handle Ajax request from "Indexation Control" page
5347 */
5348 public function meta_sync_save_seo_controls() {
5349 Metasync_Settings_Registration::instance()->meta_sync_save_seo_controls();
5350 }
5351
5352 /**
5353 * AJAX handler for saving Performance (CPU Load) settings
5354 *
5355 * Saves the CPU load threshold and returns statistics
5356 */
5357 public function ajax_save_performance_settings() {
5358 # Check nonce for security and return early if invalid
5359 if (!isset($_POST['meta_sync_nonce']) || !wp_verify_nonce($_POST['meta_sync_nonce'], 'meta_sync_general_setting_nonce')) {
5360 wp_send_json_error(array('message' => 'Invalid nonce'));
5361 return;
5362 }
5363
5364 # Check user capabilities
5365 if (!Metasync::current_user_has_plugin_access()) {
5366 wp_send_json_error(array('message' => 'Insufficient permissions'));
5367 return;
5368 }
5369
5370 # Get current options
5371 $current_options = Metasync::get_option();
5372 if (!is_array($current_options)) {
5373 $current_options = array();
5374 }
5375
5376 # Initialize performance section if it doesn't exist
5377 if (!isset($current_options['performance']) || !is_array($current_options['performance'])) {
5378 $current_options['performance'] = array();
5379 }
5380
5381 # Validate and sanitize CPU load threshold
5382 if (isset($_POST['metasync_options']['performance']['cpu_load_per_core_threshold'])) {
5383 $threshold = floatval($_POST['metasync_options']['performance']['cpu_load_per_core_threshold']);
5384 # Clamp value between 0.5 and 10.0
5385 $threshold = max(0.5, min(10.0, $threshold));
5386 $current_options['performance']['cpu_load_per_core_threshold'] = $threshold;
5387 } else {
5388 # Ensure default value exists
5389 if (!isset($current_options['performance']['cpu_load_per_core_threshold'])) {
5390 $current_options['performance']['cpu_load_per_core_threshold'] = Metasync_CPU_Monitor::DEFAULT_PER_CORE;
5391 }
5392 }
5393
5394 # Save the updated options
5395 $result = Metasync::set_option($current_options);
5396
5397 if ($result) {
5398 # Get current statistics to return
5399 $stats = Metasync_CPU_Monitor::get_stats();
5400 $cores = Metasync_CPU_Monitor::get_cpu_core_count();
5401 $effective_threshold = Metasync_CPU_Monitor::get_effective_threshold();
5402
5403 wp_send_json_success(array(
5404 'message' => 'Performance settings saved successfully!',
5405 'cpu_load_per_core_threshold' => $current_options['performance']['cpu_load_per_core_threshold'],
5406 'effective_threshold' => $effective_threshold,
5407 'cores' => $cores,
5408 'stats' => $stats
5409 ));
5410 } else {
5411 wp_send_json_error(array('message' => 'Failed to save Performance settings'));
5412 }
5413 }
5414
5415 /**
5416 * Schedule transient cleanup cron job
5417 * Runs daily to clean up expired transients and reduce database load
5418 */
5419 public function schedule_transient_cleanup_cron()
5420 {
5421 // Clear any existing scheduled event first
5422 $this->unschedule_transient_cleanup_cron();
5423
5424 // Schedule new cron job daily
5425 if (!wp_next_scheduled('metasync_cleanup_transients')) {
5426 $scheduled = wp_schedule_event(time(), 'metasync_daily_cleanup', 'metasync_cleanup_transients');
5427
5428 if (!$scheduled) {
5429 error_log('MetaSync: Failed to schedule transient cleanup cron job');
5430 }
5431 }
5432 }
5433
5434 /**
5435 * Unschedule transient cleanup cron job
5436 */
5437 public function unschedule_transient_cleanup_cron()
5438 {
5439 $timestamp = wp_next_scheduled('metasync_cleanup_transients');
5440 if ($timestamp) {
5441 wp_unschedule_event($timestamp, 'metasync_cleanup_transients');
5442 error_log('MetaSync: Transient cleanup cron job unscheduled');
5443 }
5444 }
5445
5446 /**
5447 * Maybe schedule transient cleanup cron job
5448 * Called on init hook - always schedules for database maintenance
5449 */
5450 public function maybe_schedule_transient_cleanup_cron()
5451 {
5452 if (!wp_next_scheduled('metasync_cleanup_transients')) {
5453 $this->schedule_transient_cleanup_cron();
5454 }
5455 }
5456
5457 /**
5458 * Schedule hidden post manager cron job (runs every 7 days)
5459 * Called on init hook - always schedules for template checking
5460 */
5461 public function maybe_schedule_hidden_post_check()
5462 {
5463 if (!wp_next_scheduled('metasync_hidden_post_check')) {
5464 $scheduled = wp_schedule_event(time(), 'metasync_weekly', 'metasync_hidden_post_check');
5465
5466 if ($scheduled) {
5467 error_log('MetaSync: Hidden post manager cron job scheduled successfully (runs every 7 days)');
5468 } else {
5469 error_log('MetaSync: Failed to schedule hidden post manager cron job');
5470 }
5471 }
5472 }
5473
5474 /**
5475 * Schedule OTTO 404 exclusion recheck cron job (runs daily)
5476 * Rechecks URLs auto-excluded due to 404 after 7 days; removes from exclusion if now available
5477 */
5478 public function maybe_schedule_otto_recheck_404_cron()
5479 {
5480 if (!wp_next_scheduled('metasync_otto_recheck_404_exclusions')) {
5481 $scheduled = wp_schedule_event(time(), 'metasync_daily_cleanup', 'metasync_otto_recheck_404_exclusions');
5482 if ($scheduled) {
5483 error_log('MetaSync: OTTO 404 recheck cron job scheduled successfully (runs daily)');
5484 } else {
5485 error_log('MetaSync: Failed to schedule OTTO 404 recheck cron job');
5486 }
5487 }
5488 }
5489
5490 /**
5491 * Execute transient cleanup cron job
5492 * Cleans up expired transients and plugin-specific transients to reduce database load
5493 */
5494 public function execute_transient_cleanup()
5495 {
5496 Metasync_Admin_Ajax::instance()->execute_transient_cleanup();
5497 }
5498
5499 // -------------------------------------------------------------------------
5500 // DB CLEANUP — cron scheduling, execution, render, AJAX
5501 // -------------------------------------------------------------------------
5502
5503 /**
5504 * Returns saved DB cleanup settings with defaults merged in.
5505 */
5506 private function get_db_cleanup_settings() {
5507 $defaults = array(
5508 'enabled' => false,
5509 'clean_post_revisions' => true,
5510 'clean_trashed_posts' => true,
5511 'clean_trashed_comments' => true,
5512 'clean_spam_comments' => true,
5513 'clean_expired_transients' => true,
5514 'clean_orphaned_postmeta' => true,
5515 'last_run_at' => 0,
5516 'last_run_stats' => array(),
5517 );
5518 $saved = get_option('metasync_db_cleanup_settings', array());
5519 return array_merge($defaults, $saved);
5520 }
5521
5522 /**
5523 * Schedules the weekly DB cleanup cron if the feature is enabled and not yet scheduled.
5524 */
5525 public function maybe_schedule_db_cleanup_cron() {
5526 $settings = $this->get_db_cleanup_settings();
5527 if (!empty($settings['enabled'])) {
5528 if (!wp_next_scheduled('metasync_db_cleanup')) {
5529 wp_schedule_event(time(), 'metasync_weekly', 'metasync_db_cleanup');
5530 }
5531 } else {
5532 $this->unschedule_db_cleanup_cron();
5533 }
5534 }
5535
5536 /**
5537 * Removes the DB cleanup cron event.
5538 */
5539 public function unschedule_db_cleanup_cron() {
5540 $timestamp = wp_next_scheduled('metasync_db_cleanup');
5541 if ($timestamp) {
5542 wp_unschedule_event($timestamp, 'metasync_db_cleanup');
5543 }
5544 }
5545
5546 /**
5547 * Cron callback: runs each enabled cleanup task and records stats.
5548 * Never calls wp_cache_flush() — only targeted DB deletes.
5549 */
5550 public function execute_db_cleanup() {
5551 global $wpdb;
5552
5553 $settings = $this->get_db_cleanup_settings();
5554 $stats = array();
5555 $start = microtime(true);
5556
5557 try {
5558 // 1. Post revisions
5559 if (!empty($settings['clean_post_revisions'])) {
5560 $stats['post_revisions'] = (int) $wpdb->query(
5561 "DELETE FROM {$wpdb->posts} WHERE post_type = 'revision'"
5562 );
5563 // Remove postmeta left behind by deleted revisions
5564 $wpdb->query(
5565 "DELETE pm FROM {$wpdb->postmeta} pm
5566 LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id
5567 WHERE p.ID IS NULL"
5568 );
5569 }
5570
5571 // 2. Trashed posts + their postmeta
5572 if (!empty($settings['clean_trashed_posts'])) {
5573 // Collect IDs first to cleanly remove postmeta
5574 $trashed_ids = $wpdb->get_col(
5575 "SELECT ID FROM {$wpdb->posts} WHERE post_status = 'trash'"
5576 );
5577 if (!empty($trashed_ids)) {
5578 $placeholders = implode(',', array_fill(0, count($trashed_ids), '%d'));
5579 $wpdb->query(
5580 $wpdb->prepare(
5581 "DELETE FROM {$wpdb->postmeta} WHERE post_id IN ($placeholders)",
5582 $trashed_ids
5583 )
5584 );
5585 $stats['trashed_posts'] = (int) $wpdb->query(
5586 "DELETE FROM {$wpdb->posts} WHERE post_status = 'trash'"
5587 );
5588 } else {
5589 $stats['trashed_posts'] = 0;
5590 }
5591 }
5592
5593 // 3. Trashed comments
5594 if (!empty($settings['clean_trashed_comments'])) {
5595 $stats['trashed_comments'] = (int) $wpdb->query(
5596 "DELETE FROM {$wpdb->comments} WHERE comment_approved = 'trash'"
5597 );
5598 }
5599
5600 // 4. Spam comments
5601 if (!empty($settings['clean_spam_comments'])) {
5602 $stats['spam_comments'] = (int) $wpdb->query(
5603 "DELETE FROM {$wpdb->comments} WHERE comment_approved = 'spam'"
5604 );
5605 }
5606
5607 // 5. Expired transients — direct SQL, no cache flush
5608 if (!empty($settings['clean_expired_transients'])) {
5609 // Delete timeout rows that have already expired
5610 $wpdb->query(
5611 "DELETE FROM {$wpdb->options}
5612 WHERE option_name LIKE '\_transient\_timeout\_%'
5613 AND option_value + 0 < UNIX_TIMESTAMP()"
5614 );
5615 // Delete value rows whose timeout row no longer exists
5616 $stats['expired_transients'] = (int) $wpdb->query(
5617 "DELETE o FROM {$wpdb->options} o
5618 LEFT JOIN {$wpdb->options} t
5619 ON t.option_name = CONCAT('_transient_timeout_', SUBSTRING(o.option_name, 12))
5620 WHERE o.option_name LIKE '\_transient\_%'
5621 AND o.option_name NOT LIKE '\_transient\_timeout\_%'
5622 AND t.option_id IS NULL"
5623 );
5624 }
5625
5626 // 6. Orphaned postmeta (post_id references a post that no longer exists)
5627 if (!empty($settings['clean_orphaned_postmeta'])) {
5628 $stats['orphaned_postmeta'] = (int) $wpdb->query(
5629 "DELETE pm FROM {$wpdb->postmeta} pm
5630 LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id
5631 WHERE p.ID IS NULL"
5632 );
5633 }
5634
5635 $stats['execution_ms'] = round((microtime(true) - $start) * 1000, 2);
5636
5637 // Persist last-run timestamp and stats
5638 $settings['last_run_at'] = time();
5639 $settings['last_run_stats'] = $stats;
5640 update_option('metasync_db_cleanup_settings', $settings);
5641
5642 error_log('MetaSync: DB cleanup completed — ' . json_encode($stats));
5643
5644 } catch (Exception $e) {
5645 error_log('MetaSync: DB cleanup failed — ' . $e->getMessage());
5646 }
5647 }
5648
5649 /**
5650 * Renders the Database Cleanup accordion section in Advanced Settings.
5651 */
5652 public function render_db_cleanup_section() {
5653 $settings = $this->get_db_cleanup_settings();
5654 $last_run = !empty($settings['last_run_at']) ? $settings['last_run_at'] : 0;
5655 $stats = !empty($settings['last_run_stats']) ? $settings['last_run_stats'] : array();
5656 $next_run = wp_next_scheduled('metasync_db_cleanup');
5657
5658 $task_labels = array(
5659 'clean_post_revisions' => 'Post revisions',
5660 'clean_trashed_posts' => 'Trashed posts',
5661 'clean_trashed_comments' => 'Trashed comments',
5662 'clean_spam_comments' => 'Spam comments',
5663 'clean_expired_transients' => 'Expired transients',
5664 'clean_orphaned_postmeta' => 'Orphaned post meta',
5665 );
5666 ?>
5667 <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px;">
5668 <p style="color: var(--dashboard-text-secondary); margin: 0 0 20px 0;">
5669 Remove orphaned database rows that accumulate over time and slow down queries. Runs weekly via WP-Cron when enabled.
5670 </p>
5671
5672 <form id="metasync-db-cleanup-settings-form" method="post">
5673 <?php wp_nonce_field('metasync_db_cleanup_settings_nonce', 'db_cleanup_settings_nonce'); ?>
5674
5675 <!-- Enable weekly cleanup -->
5676 <div style="background: var(--dashboard-card-bg-alt, rgba(255,255,255,0.05)); border: 1px solid var(--dashboard-border); border-radius: 8px; padding: 20px; margin-bottom: 20px;">
5677 <label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
5678 <input type="checkbox"
5679 id="db_cleanup_enabled"
5680 name="enabled"
5681 value="1"
5682 <?php checked(!empty($settings['enabled'])); ?>
5683 style="width: 16px; height: 16px; cursor: pointer;" />
5684 <span style="color: var(--dashboard-text-primary); font-weight: 600; font-size: 14px;">
5685 Enable Weekly AI Cleanup
5686 </span>
5687 </label>
5688 <p style="color: var(--dashboard-text-secondary); font-size: 12px; margin: 8px 0 0 26px;">
5689 Schedules an automatic cleanup every 7 days via WP-Cron.
5690 <?php if ($next_run): ?>
5691 Next run: <strong style="color: var(--dashboard-text-primary);"><?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $next_run)); ?></strong>
5692 <?php endif; ?>
5693 </p>
5694 </div>
5695
5696 <!-- Cleanup tasks -->
5697 <div style="background: var(--dashboard-card-bg-alt, rgba(255,255,255,0.05)); border: 1px solid var(--dashboard-border); border-radius: 8px; padding: 20px; margin-bottom: 20px;">
5698 <h3 style="color: var(--dashboard-text-primary); margin: 0 0 16px 0; font-size: 16px; font-weight: 600;">Cleanup Tasks</h3>
5699 <?php foreach ($task_labels as $key => $label): ?>
5700 <label style="display: flex; align-items: center; gap: 10px; margin-bottom: 12px; cursor: pointer;">
5701 <input type="checkbox"
5702 name="<?php echo esc_attr($key); ?>"
5703 value="1"
5704 <?php checked(!empty($settings[$key])); ?>
5705 style="width: 16px; height: 16px; cursor: pointer;" />
5706 <span style="color: var(--dashboard-text-primary); font-size: 14px;"><?php echo esc_html($label); ?></span>
5707 </label>
5708 <?php endforeach; ?>
5709 </div>
5710
5711 <!-- Last run info -->
5712 <div id="metasync-db-cleanup-last-run"
5713 style="background: var(--dashboard-card-bg-alt, rgba(255,255,255,0.05)); border: 1px solid var(--dashboard-border); border-radius: 8px; padding: 20px; margin-bottom: 20px; <?php echo $last_run ? '' : 'display:none;'; ?>">
5714 <h3 style="color: var(--dashboard-text-primary); margin: 0 0 12px 0; font-size: 16px; font-weight: 600;">Last Cleanup</h3>
5715 <p id="metasync-db-cleanup-last-run-time" style="color: var(--dashboard-text-secondary); font-size: 13px; margin: 0 0 10px 0;">
5716 <?php echo $last_run ? esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $last_run)) : ''; ?>
5717 </p>
5718 <div id="metasync-db-cleanup-stats" style="display: flex; flex-wrap: wrap; gap: 10px;">
5719 <?php foreach ($stats as $stat_key => $count): ?>
5720 <?php if ($stat_key === 'execution_ms') continue; ?>
5721 <span style="background: rgba(34,197,94,0.1); border: 1px solid rgba(34,197,94,0.3); color: #22c55e; padding: 4px 10px; border-radius: 4px; font-size: 12px;">
5722 <?php echo esc_html(str_replace('_', ' ', $stat_key)); ?>: <?php echo intval($count); ?>
5723 </span>
5724 <?php endforeach; ?>
5725 <?php if (!empty($stats['execution_ms'])): ?>
5726 <span style="color: var(--dashboard-text-secondary); font-size: 12px; align-self: center;">
5727 in <?php echo esc_html($stats['execution_ms']); ?>ms
5728 </span>
5729 <?php endif; ?>
5730 </div>
5731 </div>
5732
5733 <!-- Buttons -->
5734 <div style="display: flex; gap: 12px; align-items: center; margin-top: 4px;">
5735 <button type="submit"
5736 id="metasync-db-cleanup-save-btn"
5737 class="button button-primary"
5738 style="padding: 10px 20px; font-size: 14px; font-weight: 500;">
5739 <span class="save-text">Save Settings</span>
5740 <span class="save-spinner" style="display:none; margin-left: 8px;"></span>
5741 </button>
5742 <button type="button"
5743 id="metasync-db-cleanup-run-btn"
5744 class="button"
5745 style="padding: 10px 20px; font-size: 14px; font-weight: 500;">
5746 <span class="run-text">Run Cleanup Now</span>
5747 <span class="run-spinner" style="display:none; margin-left: 8px;"></span>
5748 </button>
5749 </div>
5750
5751 <!-- Messages -->
5752 <div id="metasync-db-cleanup-message" style="display:none; margin-top: 16px; padding: 12px; border-radius: 6px;"></div>
5753 </form>
5754 </div>
5755
5756 <script>
5757 jQuery(document).ready(function($) {
5758 var $form = $('#metasync-db-cleanup-settings-form');
5759 var $saveBtn = $('#metasync-db-cleanup-save-btn');
5760 var $runBtn = $('#metasync-db-cleanup-run-btn');
5761 var $message = $('#metasync-db-cleanup-message');
5762
5763 function showMessage(text, type) {
5764 $message.css({
5765 'background' : type === 'success' ? 'rgba(34,197,94,0.1)' : 'rgba(239,68,68,0.1)',
5766 'border' : '1px solid ' + (type === 'success' ? 'rgba(34,197,94,0.3)' : 'rgba(239,68,68,0.3)'),
5767 'color' : type === 'success' ? '#22c55e' : '#ef4444',
5768 'padding' : '12px 16px',
5769 'border-radius' : '6px',
5770 'font-size' : '14px',
5771 'line-height': '1.5',
5772 'display' : 'block'
5773 }).html('<strong style="margin-right:8px;">' + (type === 'success' ? '' : '') + '</strong>' + text).show();
5774
5775 if (type === 'success') {
5776 setTimeout(function() { $message.fadeOut(300); }, 5000);
5777 }
5778 }
5779
5780 // Save settings
5781 $form.on('submit', function(e) {
5782 e.preventDefault();
5783 $saveBtn.prop('disabled', true);
5784 $saveBtn.find('.save-text').text('Saving...');
5785 $saveBtn.find('.save-spinner').show();
5786 $message.hide();
5787
5788 $.ajax({
5789 url : ajaxurl,
5790 type : 'POST',
5791 data : $form.serialize() + '&action=metasync_save_db_cleanup_settings',
5792 success: function(response) {
5793 $saveBtn.prop('disabled', false);
5794 $saveBtn.find('.save-text').text('Save Settings');
5795 $saveBtn.find('.save-spinner').hide();
5796 if (response.success) {
5797 showMessage(response.data.message, 'success');
5798 // Update next-run label if returned
5799 if (response.data.next_run_label) {
5800 $('#db_cleanup_enabled').closest('label')
5801 .next('p').find('strong').text(response.data.next_run_label);
5802 }
5803 } else {
5804 showMessage(response.data.message || 'Error saving settings.', 'error');
5805 }
5806 },
5807 error: function() {
5808 $saveBtn.prop('disabled', false);
5809 $saveBtn.find('.save-text').text('Save Settings');
5810 $saveBtn.find('.save-spinner').hide();
5811 showMessage('An error occurred. Please try again.', 'error');
5812 }
5813 });
5814 });
5815
5816 // Run cleanup now — sends current form state so unsaved changes are respected
5817 $runBtn.on('click', function() {
5818 $runBtn.prop('disabled', true);
5819 $runBtn.find('.run-text').text('Running...');
5820 $runBtn.find('.run-spinner').show();
5821 $message.hide();
5822
5823 $.ajax({
5824 url : ajaxurl,
5825 type : 'POST',
5826 data : $form.serialize() + '&action=metasync_run_db_cleanup',
5827 success: function(response) {
5828 $runBtn.prop('disabled', false);
5829 $runBtn.find('.run-text').text('Run Cleanup Now');
5830 $runBtn.find('.run-spinner').hide();
5831
5832 if (response.success) {
5833 showMessage(response.data.message, 'success');
5834
5835 // Update last-run panel
5836 if (response.data.timestamp_label) {
5837 $('#metasync-db-cleanup-last-run').show();
5838 $('#metasync-db-cleanup-last-run-time').text(response.data.timestamp_label);
5839 }
5840 if (response.data.stats_html) {
5841 $('#metasync-db-cleanup-stats').html(response.data.stats_html);
5842 }
5843 } else {
5844 showMessage(response.data.message || 'Cleanup failed.', 'error');
5845 }
5846 },
5847 error: function() {
5848 $runBtn.prop('disabled', false);
5849 $runBtn.find('.run-text').text('Run Cleanup Now');
5850 $runBtn.find('.run-spinner').hide();
5851 showMessage('An error occurred. Please try again.', 'error');
5852 }
5853 });
5854 });
5855 });
5856 </script>
5857 <?php
5858 }
5859
5860 /**
5861 * AJAX: Save DB cleanup settings and reschedule cron accordingly.
5862 */
5863 public function ajax_save_db_cleanup_settings() {
5864 if (!isset($_POST['db_cleanup_settings_nonce']) ||
5865 !wp_verify_nonce($_POST['db_cleanup_settings_nonce'], 'metasync_db_cleanup_settings_nonce')) {
5866 wp_send_json_error(array('message' => 'Invalid security token. Please refresh the page and try again.'));
5867 return;
5868 }
5869
5870 if (!Metasync::current_user_has_plugin_access()) {
5871 wp_send_json_error(array('message' => 'Insufficient permissions.'));
5872 return;
5873 }
5874
5875 $existing = $this->get_db_cleanup_settings();
5876
5877 $task_keys = array(
5878 'clean_post_revisions',
5879 'clean_trashed_posts',
5880 'clean_trashed_comments',
5881 'clean_spam_comments',
5882 'clean_expired_transients',
5883 'clean_orphaned_postmeta',
5884 );
5885
5886 $new_settings = array(
5887 'enabled' => !empty($_POST['enabled']),
5888 'last_run_at' => $existing['last_run_at'],
5889 'last_run_stats' => $existing['last_run_stats'],
5890 );
5891
5892 foreach ($task_keys as $key) {
5893 $new_settings[$key] = !empty($_POST[$key]);
5894 }
5895
5896 update_option('metasync_db_cleanup_settings', $new_settings);
5897
5898 // Reschedule based on new enabled state
5899 if (!empty($new_settings['enabled'])) {
5900 if (!wp_next_scheduled('metasync_db_cleanup')) {
5901 wp_schedule_event(time(), 'metasync_weekly', 'metasync_db_cleanup');
5902 }
5903 $next_run = wp_next_scheduled('metasync_db_cleanup');
5904 $next_run_label = $next_run
5905 ? date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $next_run)
5906 : '';
5907 wp_send_json_success(array(
5908 'message' => 'Settings saved. Weekly cleanup is enabled.',
5909 'next_run_label' => $next_run_label,
5910 ));
5911 } else {
5912 $this->unschedule_db_cleanup_cron();
5913 wp_send_json_success(array(
5914 'message' => 'Settings saved. Weekly cleanup is disabled.',
5915 ));
5916 }
5917 }
5918
5919 /**
5920 * AJAX: Manually trigger the DB cleanup and return stats for the UI.
5921 * Persists current form state first so unsaved checkbox changes are respected.
5922 */
5923 public function ajax_run_db_cleanup() {
5924 if (!isset($_POST['db_cleanup_settings_nonce']) ||
5925 !wp_verify_nonce($_POST['db_cleanup_settings_nonce'], 'metasync_db_cleanup_settings_nonce')) {
5926 wp_send_json_error(array('message' => 'Invalid security token.'));
5927 return;
5928 }
5929
5930 if (!Metasync::current_user_has_plugin_access()) {
5931 wp_send_json_error(array('message' => 'Insufficient permissions.'));
5932 return;
5933 }
5934
5935 // Save current form state before running so the cleanup uses what the user sees
5936 $existing = $this->get_db_cleanup_settings();
5937 $task_keys = array(
5938 'clean_post_revisions',
5939 'clean_trashed_posts',
5940 'clean_trashed_comments',
5941 'clean_spam_comments',
5942 'clean_expired_transients',
5943 'clean_orphaned_postmeta',
5944 );
5945 $to_save = array(
5946 'enabled' => !empty($_POST['enabled']),
5947 'last_run_at' => $existing['last_run_at'],
5948 'last_run_stats' => $existing['last_run_stats'],
5949 );
5950 foreach ($task_keys as $key) {
5951 $to_save[$key] = !empty($_POST[$key]);
5952 }
5953 update_option('metasync_db_cleanup_settings', $to_save);
5954
5955 // Reschedule cron to match the (possibly updated) enabled state
5956 if (!empty($to_save['enabled'])) {
5957 if (!wp_next_scheduled('metasync_db_cleanup')) {
5958 wp_schedule_event(time(), 'metasync_weekly', 'metasync_db_cleanup');
5959 }
5960 } else {
5961 $this->unschedule_db_cleanup_cron();
5962 }
5963
5964 $this->execute_db_cleanup();
5965
5966 $settings = $this->get_db_cleanup_settings();
5967 $stats = $settings['last_run_stats'];
5968 $timestamp_label = date_i18n(
5969 get_option('date_format') . ' ' . get_option('time_format'),
5970 $settings['last_run_at']
5971 );
5972
5973 // Build stats badges HTML
5974 $stat_labels = array(
5975 'post_revisions' => 'Post revisions',
5976 'trashed_posts' => 'Trashed posts',
5977 'trashed_comments' => 'Trashed comments',
5978 'spam_comments' => 'Spam comments',
5979 'expired_transients' => 'Expired transients',
5980 'orphaned_postmeta' => 'Orphaned post meta',
5981 );
5982
5983 $stats_html = '';
5984 foreach ($stat_labels as $key => $label) {
5985 if (isset($stats[$key])) {
5986 $stats_html .= '<span style="background:rgba(34,197,94,0.1);border:1px solid rgba(34,197,94,0.3);color:#22c55e;padding:4px 10px;border-radius:4px;font-size:12px;">'
5987 . esc_html($label) . ': ' . intval($stats[$key]) . '</span> ';
5988 }
5989 }
5990 if (!empty($stats['execution_ms'])) {
5991 $stats_html .= '<span style="color:var(--dashboard-text-secondary);font-size:12px;align-self:center;">in '
5992 . esc_html($stats['execution_ms']) . 'ms</span>';
5993 }
5994
5995 wp_send_json_success(array(
5996 'message' => 'Cleanup completed successfully.',
5997 'timestamp_label' => $timestamp_label,
5998 'stats_html' => $stats_html,
5999 ));
6000 }
6001
6002 /**
6003 * Control plugin auto-updates based on user setting
6004 *
6005 * @param bool $update Whether to update
6006 * @param object $item Update offer
6007 * @return bool Whether to allow auto-update
6008 */
6009 public function control_plugin_auto_updates($update, $item)
6010 {
6011 # Check if the item object has the slug property
6012 if (!isset($item->slug)) {
6013 return $update;
6014 }
6015
6016 // Check if this is our plugin
6017 if ($item->slug === 'metasync') {
6018 $general_settings = Metasync::get_option('general') ?? [];
6019 $enable_auto_updates = $general_settings['enable_auto_updates'] ?? false;
6020
6021 // Return the user's preference (true = allow auto-updates, false = prevent)
6022 return $enable_auto_updates === 'true' || $enable_auto_updates === true;
6023 }
6024
6025 // For other plugins, don't interfere with their auto-update settings
6026 return $update;
6027 }
6028
6029 /**
6030 * AJAX handler to add excluded URL for OTTO
6031 */
6032 public function ajax_otto_add_excluded_url()
6033 {
6034 Metasync_Otto_Cache_Manager::instance()->ajax_otto_add_excluded_url();
6035 }
6036
6037 /**
6038 * AJAX handler to delete excluded URL for OTTO
6039 */
6040 public function ajax_otto_delete_excluded_url()
6041 {
6042 Metasync_Otto_Cache_Manager::instance()->ajax_otto_delete_excluded_url();
6043 }
6044
6045 /**
6046 * AJAX handler to recheck if an excluded URL is now available
6047 * Used for "Recheck" action on Excluded URLs list
6048 */
6049 public function ajax_otto_recheck_excluded_url()
6050 {
6051 Metasync_Otto_Cache_Manager::instance()->ajax_otto_recheck_excluded_url();
6052 }
6053
6054 /**
6055 * AJAX handler to get excluded URLs with pagination
6056 */
6057 public function ajax_otto_get_excluded_urls()
6058 {
6059 Metasync_Otto_Cache_Manager::instance()->ajax_otto_get_excluded_urls();
6060 }
6061
6062 /**
6063 * AJAX handler for submitting issue reports to Sentry
6064 *
6065 * @since 2.5.10
6066 * @return void Sends JSON response and exits
6067 */
6068 public function ajax_submit_issue_report()
6069 {
6070 Metasync_Admin_Ajax::instance()->ajax_submit_issue_report();
6071 }
6072
6073 /**
6074 * Format duration seconds into human-readable label
6075 *
6076 * @since 2.5.11
6077 * @param int $seconds Duration in seconds
6078 * @return string Human-readable duration
6079 */
6080 private function format_duration_label($seconds)
6081 {
6082 $labels = array(
6083 3600 => '1 hour',
6084 14400 => '4 hours',
6085 28800 => '8 hours',
6086 86400 => '24 hours',
6087 172800 => '48 hours',
6088 604800 => '7 days',
6089 1209600 => '14 days',
6090 2592000 => '30 days'
6091 );
6092
6093 if (isset($labels[$seconds])) {
6094 return $labels[$seconds];
6095 }
6096
6097 # Calculate hours if not a standard duration
6098 $hours = round($seconds / 3600);
6099 return $hours . ' hours';
6100 }
6101
6102 /**
6103 * AJAX handler for password recovery
6104 * Sends the whitelabel settings password to the configured recovery email
6105 */
6106 public function ajax_recover_password()
6107 {
6108 Metasync_Admin_Ajax::instance()->ajax_recover_password();
6109 }
6110
6111 /**
6112 * AJAX handler for saving theme preference
6113 * Saves the user's theme choice (light/dark) to WordPress options
6114 */
6115 public function ajax_save_theme()
6116 {
6117 Metasync_Admin_Ajax::instance()->ajax_save_theme();
6118 }
6119
6120 /**
6121 * AJAX handler for tracking 1-click activation in GA4
6122 */
6123 public function ajax_track_one_click_activation()
6124 {
6125 Metasync_Admin_Ajax::instance()->ajax_track_one_click_activation();
6126 }
6127
6128 /**
6129 * Handler for exporting whitelabel settings to a zip file
6130 * Uses admin-post action for file downloads
6131 */
6132 public function handle_export_whitelabel_settings()
6133 {
6134 Metasync_Admin_Ajax::instance()->handle_export_whitelabel_settings();
6135 }
6136
6137
6138 /**
6139 * Add custom column to posts/pages list for HTML-converted pages
6140 *
6141 * @param array $columns Existing columns
6142 * @return array Modified columns
6143 */
6144 public function add_html_converted_column($columns)
6145 {
6146 // Add column after the title column
6147 $new_columns = array();
6148 foreach ($columns as $key => $value) {
6149 $new_columns[$key] = $value;
6150 if ($key === 'title') {
6151 $new_columns['metasync_html_source'] = __('Source', 'metasync');
6152 }
6153 }
6154 return $new_columns;
6155 }
6156
6157 /**
6158 * Render content for the HTML-converted column
6159 *
6160 * @param string $column_name Name of the column
6161 * @param int $post_id Post ID
6162 */
6163 public function render_html_converted_column($column_name, $post_id)
6164 {
6165 if ($column_name !== 'metasync_html_source') {
6166 return;
6167 }
6168
6169 // Check if this is an HTML-converted page
6170 $has_raw_html = get_post_meta($post_id, '_metasync_raw_html_enabled', true);
6171 $has_custom_css = get_post_meta($post_id, '_metasync_custom_css', true);
6172
6173 // If page has raw HTML or custom CSS from conversion, show badge
6174 if ($has_raw_html || !empty($has_custom_css)) {
6175 $label = $this->get_html_source_label();
6176 $tooltip = sprintf(
6177 __('This page was created using %s HTML-to-Builder converter', 'metasync'),
6178 $label
6179 );
6180
6181 echo sprintf(
6182 '<span class="metasync-html-badge" title="%s">
6183 <span class="metasync-badge-icon">⚡</span>
6184 <span class="metasync-badge-text">%s</span>
6185 </span>',
6186 esc_attr($tooltip),
6187 esc_html($label)
6188 );
6189 }
6190 }
6191
6192 /**
6193 * Get the label for HTML-converted pages (respects whitelabel settings)
6194 *
6195 * @return string Label to display
6196 */
6197 private function get_html_source_label()
6198 {
6199 $whitelabel_company = Metasync::get_whitelabel_company_name();
6200 if (!empty($whitelabel_company)) {
6201 return $whitelabel_company . ' AI';
6202 }
6203 return Metasync::get_effective_plugin_name() . ' AI';
6204 }
6205
6206 /**
6207 * Add source notice banner in the page editor
6208 *
6209 * @param WP_Post $post Current post object
6210 */
6211 public function add_editor_source_notice($post)
6212 {
6213 if (!$post || !in_array($post->post_type, array('post', 'page'))) {
6214 return;
6215 }
6216
6217 // Don't show the "HTML-to-Builder converter" banner on LPS-synced / custom-HTML
6218 // pages: those are raw-HTML store-and-serve pages, NOT actually converted to a
6219 // page builder, so the banner mislabels them. It still shows for pages genuinely
6220 // produced by the converter.
6221 if (function_exists('metasync_is_custom_or_lps_page') && metasync_is_custom_or_lps_page($post->ID)) {
6222 return;
6223 }
6224
6225 $has_raw_html = get_post_meta($post->ID, '_metasync_raw_html_enabled', true);
6226 $has_custom_css = get_post_meta($post->ID, '_metasync_custom_css', true);
6227
6228 if ($has_raw_html || !empty($has_custom_css)) {
6229 $label = $this->get_html_source_label();
6230 $message = sprintf(
6231 __('This page was created using %s HTML-to-Builder converter. The design is preserved with custom CSS and inline styles.', 'metasync'),
6232 '<strong>' . esc_html($label) . '</strong>'
6233 );
6234
6235 echo sprintf(
6236 '<div class="metasync-editor-notice notice notice-info is-dismissible">
6237 <div class="metasync-editor-notice-content">
6238 <span class="metasync-editor-badge">
6239 <span class="metasync-badge-icon">⚡</span>
6240 <span class="metasync-badge-text">%s</span>
6241 </span>
6242 <p class="metasync-editor-message">%s</p>
6243 </div>
6244 </div>',
6245 esc_html($label),
6246 $message
6247 );
6248 }
6249 }
6250
6251 /**
6252 * Add source display in quick edit panel
6253 *
6254 * @param string $column_name Column name
6255 * @param string $post_type Post type
6256 */
6257 public function add_quick_edit_source_display($column_name, $post_type)
6258 {
6259 if ($column_name !== 'metasync_html_source') {
6260 return;
6261 }
6262
6263 if (!in_array($post_type, array('post', 'page'))) {
6264 return;
6265 }
6266
6267 ?>
6268 <fieldset class="inline-edit-col-left metasync-quick-edit-source">
6269 <div class="inline-edit-col">
6270 <label>
6271 <span class="title"><?php _e('Source', 'metasync'); ?></span>
6272 <span class="metasync-quick-edit-badge-container"></span>
6273 </label>
6274 </div>
6275 </fieldset>
6276 <?php
6277 }
6278
6279 /**
6280 * Add dashboard widget for HTML-converted pages
6281 */
6282 public function add_html_pages_dashboard_widget()
6283 {
6284 $label = $this->get_html_source_label();
6285 $widget_title = sprintf(__('%s Pages', 'metasync'), $label);
6286
6287 wp_add_dashboard_widget(
6288 'metasync_html_pages_widget',
6289 $widget_title,
6290 array($this, 'render_html_pages_dashboard_widget')
6291 );
6292 }
6293
6294 /**
6295 * Render the dashboard widget content
6296 */
6297 public function render_html_pages_dashboard_widget()
6298 {
6299 Metasync_Admin_Ajax::instance()->render_html_pages_dashboard_widget();
6300 }
6301
6302 /**
6303 * Bot Statistics page callback
6304 * Displays bot detection statistics and logs
6305 */
6306 public function create_admin_bot_statistics_page()
6307 {
6308 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-otto-bot-statistics.php';
6309 }
6310
6311 /**
6312 * AJAX handler for resetting bot statistics
6313 */
6314 public function ajax_reset_bot_stats()
6315 {
6316 check_ajax_referer('metasync_reset_bot_stats', 'nonce');
6317
6318 if (!Metasync::current_user_has_plugin_access()) {
6319 wp_send_json_error(['message' => 'Insufficient permissions.']);
6320 }
6321
6322 require_once plugin_dir_path(dirname(__FILE__)) . 'otto/class-metasync-otto-bot-statistics-database.php';
6323 $db = Metasync_Otto_Bot_Statistics_Database::get_instance();
6324
6325 $result = $db->reset_statistics();
6326
6327 if ($result) {
6328 wp_send_json_success(['message' => 'Statistics reset successfully.']);
6329 } else {
6330 wp_send_json_error(['message' => 'Failed to reset statistics.']);
6331 }
6332 }
6333
6334 /**
6335 * AJAX handler for sending URLs to Google Instant Indexing API
6336 *
6337 * @since 2.6.0
6338 * @return void Sends JSON response and exits
6339 */
6340 public function ajax_send_giapi()
6341 {
6342 check_ajax_referer('metasync_nonce', 'nonce');
6343
6344 if (!Metasync::current_user_has_plugin_access()) {
6345 wp_send_json_error(['message' => 'Insufficient permissions.'], 403);
6346 }
6347
6348 $post_data = metasync_sanitize_input_array($_POST);
6349 if (!isset($post_data['metasync_giapi_url'])) {
6350 return;
6351 }
6352
6353 // Parse URLs from textarea input (one per line)
6354 $urls = array_values(array_filter(array_map('trim', explode("\n", sanitize_textarea_field(wp_unslash($post_data['metasync_giapi_url']))))));
6355
6356 if (empty($urls)) {
6357 return;
6358 }
6359
6360 if (!isset($post_data['metasync_giapi_action'])) {
6361 return;
6362 }
6363 $action = sanitize_title($post_data['metasync_giapi_action']);
6364
6365 // Map form action values to google_index_direct action values
6366 if ($action === 'remove') {
6367 $action = 'delete';
6368 }
6369
6370 header('Content-type: application/json');
6371
6372 $result_data = [];
6373 foreach ($urls as $i => $url) {
6374 $url = esc_url_raw($url);
6375 if (empty($url)) {
6376 continue;
6377 }
6378
6379 if ($action === 'status') {
6380 $result = google_index_direct()->get_url_status($url);
6381 } else {
6382 $result = google_index_direct()->index_url($url, $action);
6383 }
6384
6385 $key = 'url-' . $i;
6386 if (!empty($result['success'])) {
6387 $result_data[$key] = $result['data'];
6388 } else {
6389 $result_data[$key] = (object) [
6390 'error' => (object) [
6391 'code' => isset($result['error']['code']) ? $result['error']['code'] : 400,
6392 'message' => isset($result['error']['message']) ? $result['error']['message'] : 'Unknown error',
6393 ]
6394 ];
6395 }
6396 }
6397
6398 // For single URL, unwrap from the batch format (matches old behavior)
6399 if (count($result_data) === 1) {
6400 $result_data = reset($result_data);
6401 }
6402
6403 wp_send_json($result_data);
6404 wp_die();
6405 }
6406
6407 /**
6408 * AJAX handler for sending URLs to Bing via IndexNow API
6409 *
6410 * @since 2.6.0
6411 * @return void Sends JSON response and exits
6412 */
6413 public function ajax_send_bing_indexnow()
6414 {
6415 check_ajax_referer('metasync_nonce', 'nonce');
6416
6417 if (!Metasync::current_user_has_plugin_access()) {
6418 wp_send_json_error(['message' => 'Insufficient permissions.'], 403);
6419 }
6420
6421 require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php';
6422 $bing_instant_index = new Metasync_Bing_Instant_Index();
6423 $bing_instant_index->send();
6424 }
6425
6426 /**
6427 * Save instant indexing settings (Google and Bing)
6428 *
6429 * @since 2.6.0
6430 * @return void
6431 */
6432 public function save_instant_indexing_settings()
6433 {
6434 // Check if this is a settings submission
6435 if (!isset($_POST['submit'])) {
6436 return;
6437 }
6438
6439 // This handler runs on admin_init, which also fires inside
6440 // admin-ajax.php before its login gate, so a bare isset() check let
6441 // any request (including logged-out ones) rewrite the auto-submit
6442 // post types. Require the nonce and plugin access before writing.
6443 if (!isset($_POST['metasync_instant_indexing_nonce'])
6444 || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['metasync_instant_indexing_nonce'])), 'metasync_instant_indexing_settings')
6445 || !Metasync::current_user_has_plugin_access()) {
6446 return;
6447 }
6448
6449 // Save post types for Google Instant Indexing auto-submit
6450 if (isset($_POST['metasync_post_types'])) {
6451 $post_data = metasync_sanitize_input_array($_POST);
6452 $post_types = is_array($post_data['metasync_post_types']) ? array_map('sanitize_title', $post_data['metasync_post_types']) : [];
6453
6454 $settings = get_option('metasync_options_instant_indexing', ['post_types' => []]);
6455 $settings['post_types'] = array_values($post_types);
6456 update_option('metasync_options_instant_indexing', $settings);
6457 }
6458
6459 // Note: Bing Instant Indexing settings are saved via AJAX in save_bing_inline_settings_ajax()
6460 }
6461
6462 /**
6463 * Save Bing instant indexing settings from inline form (Indexation Control page)
6464 *
6465 * @since 2.6.0
6466 * @return bool True on success, false on failure
6467 */
6468 private function save_bing_inline_settings_ajax() {
6469 return Metasync_Settings_Registration::instance()->save_bing_inline_settings_ajax();
6470 }
6471
6472 /**
6473 * Add instant indexing action links to post/page rows
6474 *
6475 * @since 2.6.0
6476 * @param array $actions Current actions
6477 * @param WP_Post $post Current post object
6478 * @return array Modified actions
6479 */
6480 public function add_instant_indexing_post_actions($actions, $post)
6481 {
6482 // Add Google Instant Indexing links
6483 $options = get_option('metasync_options_instant_indexing', ['json_key' => '', 'post_types' => []]);
6484 $post_types = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : [];
6485
6486 if (in_array($post->post_type, $post_types) && $post->post_status == 'publish') {
6487 $link = get_permalink($post);
6488
6489 // Get menu slug (support white label)
6490 $general_options = Metasync::get_option('general') ?? [];
6491 $menu_slug = !empty($general_options['white_label_plugin_menu_slug']) ? $general_options['white_label_plugin_menu_slug'] : 'searchatlas';
6492 $page_slug = $menu_slug . '-google-console';
6493
6494 $actions['index-update'] = '<a href="' . admin_url("admin.php?page=" . $page_slug . "&postaction=update&posturl=" . rawurlencode($link)) . '" title="" rel="permalink">Update Google Index</a>';
6495 $actions['index-status'] = '<a href="' . admin_url("admin.php?page=" . $page_slug . "&postaction=status&posturl=" . rawurlencode($link)) . '" title="" rel="permalink">Status Google Index</a>';
6496 }
6497
6498 // Add Bing Instant Indexing links
6499 require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php';
6500 $bing_instant_index = new Metasync_Bing_Instant_Index();
6501 $actions = $bing_instant_index->bing_instant_index_post_link($actions, $post);
6502
6503 return $actions;
6504 }
6505
6506 /**
6507 * Auto-submit post to instant indexing services when published
6508 *
6509 * @since 2.6.0
6510 * @param int $post_id Post ID
6511 * @param WP_Post $post Post object
6512 * @param bool $update Whether this is an update
6513 * @return void
6514 */
6515 public function auto_submit_to_instant_indexing($post_id, $post, $update)
6516 {
6517 // Skip revisions, autosaves, and non-published posts
6518 if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) {
6519 return;
6520 }
6521 if ($post->post_status !== 'publish') {
6522 return;
6523 }
6524 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
6525 return;
6526 }
6527
6528 // Auto-submit to Google Instant Indexing
6529 $seo_controls = Metasync::get_option('seo_controls');
6530 if (!empty($seo_controls['enable_googleinstantindex']) && $seo_controls['enable_googleinstantindex'] === 'true') {
6531 $options = get_option('metasync_options_instant_indexing', ['post_types' => []]);
6532 $post_types = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : [];
6533
6534 if (in_array($post->post_type, $post_types)) {
6535 // save_post is the single owner of auto-submit and also fires
6536 // on REST-created posts, where nothing else may have loaded
6537 // the Google Index helpers yet — load them on demand instead
6538 // of silently skipping.
6539 if (!function_exists('google_index_direct')) {
6540 $google_index_path = plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php';
6541 if (file_exists($google_index_path)) {
6542 require_once $google_index_path;
6543 }
6544 }
6545 if (function_exists('google_index_direct')) {
6546 $service_info = google_index_direct()->get_service_account_info();
6547 if (!isset($service_info['error'])) {
6548 google_index_direct()->index_post($post_id, $post->post_type, 'update');
6549 }
6550 }
6551 }
6552 }
6553
6554 // Auto-submit to Bing Instant Indexing
6555 require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php';
6556 $bing_instant_index = new Metasync_Bing_Instant_Index();
6557 $bing_instant_index->auto_submit_on_publish($post_id, $post, $update);
6558 }
6559 }
6560