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

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