PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.16
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.16
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 2.6.16, at admin/class-metasync-admin.php

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