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

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