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

6,478 lines 268.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // If this file is called directly, abort.
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7
8 /**
9 * The admin-specific functionality of the plugin.
10 *
11 * @link https://searchatlas.com
12 * @since 1.0.0
13 *
14 * @package Metasync
15 * @subpackage Metasync/admin
16 */
17
18 /**
19 * The admin-specific functionality of the plugin.
20 *
21 * Defines the plugin name, version, and two examples hooks for how to
22 * enqueue the admin-specific stylesheet and JavaScript.
23 *
24 * @package Metasync
25 * @subpackage Metasync/admin
26 * @author Engineering Team <support@searchatlas.com>
27 */
28 class Metasync_Admin
29 {
30
31 // Section constants for settings
32 const SECTION_FEATURES = "features_settings";
33 const SECTION_METASYNC = "metasync_settings";
34 const SECTION_SEARCHENGINE = "searchengine_settings";
35 const SECTION_LOCALSEO = "local_seo";
36 const SECTION_CODESNIPPETS = "code_snippets";
37 const SECTION_OPTIMAL_SETTINGS = "optimal_settings";
38 const SECTION_SITE_SETTINGS = "site_settings";
39 const SECTION_COMMON_SETTINGS = "common_settings";
40 const SECTION_COMMON_META_SETTINGS = "common_meta_settings";
41 const SECTION_SOCIAL_META = "social_meta";
42 const SECTION_SEO_CONTROLS = "seo_controls";
43 const SECTION_SEO_CONTROLS_ADVANCED = "seo_controls_advanced";
44 const SECTION_SEO_CONTROLS_INSTANT_INDEX = "seo_controls_instant_index";
45 const SECTION_PLUGIN_VISIBILITY = "plugin_visibility_settings";
46 const SECTION_BREADCRUMBS = "breadcrumbs_settings";
47 const SECTION_LLMS_TXT = "llms_txt_settings";
48
49 /**
50 * The ID of this plugin.
51 *
52 * @since 1.0.0
53 * @access private
54 * @var string $plugin_name The ID of this plugin.
55 */
56 private $plugin_name;
57
58 /**
59 * The version of this plugin.
60 *
61 * @since 1.0.0
62 * @access private
63 * @var string $version The current version of this plugin.
64 */
65 private $version;
66
67 /**
68 * Holds the values to be used in the fields callbacks
69 */
70 private $options;
71 public $menu_title = "Search Atlas"; // Default, overridden by get_effective_menu_title()
72 public const page_title = "MetaSync Settings";
73
74 /**
75 * Get effective menu title with whitelabel company name
76 */
77 public function get_effective_menu_title()
78 {
79 $whitelabel_company_name = Metasync::get_whitelabel_company_name();
80
81 if ($whitelabel_company_name) {
82 return $whitelabel_company_name . ' SEO';
83 }
84
85 // Use centralized method for getting effective plugin name
86 return Metasync::get_effective_plugin_name();
87 }
88 public const option_group = "metasync_group";
89 public const option_key = "metasync_options";
90 public static $page_slug = "searchatlas";
91 /**
92 * Get the effective dashboard domain
93 * Returns whitelabel domain if set, otherwise returns default production domain
94 * Uses centralized constant from main Metasync class
95 */
96 public static function get_effective_dashboard_domain()
97 {
98 // Delegate to main Metasync class for consistent domain resolution
99 return Metasync::get_dashboard_domain();
100 }
101
102 /**
103 * Static method to render header and navigation for external pages (like AI Agent)
104 * This creates a minimal instance just for rendering
105 */
106 public static function render_standard_header_nav($page_title = null, $current_page = null) {
107 Metasync_Admin_Navigation::instance()->render_standard_header_nav($page_title, $current_page);
108 }
109
110 /**
111 * Static header render
112 */
113 public static function render_static_header($page_title = null) {
114 Metasync_Admin_Navigation::instance()->render_static_header($page_title);
115 }
116
117 /**
118 * Static navigation render
119 */
120 public static function render_static_navigation($current_page = null) {
121 Metasync_Admin_Navigation::instance()->render_static_navigation($current_page);
122 }
123
124 /**
125 * Get dashboard URL with authentication tokens and tracking parameters
126 * Returns the complete URL for accessing the Search Atlas dashboard
127 */
128 public function get_dashboard_url()
129 {
130 // Get the effective dashboard domain (whitelabel or production)
131 $dashboard_url = self::get_effective_dashboard_domain();
132
133 // Get current options for token inclusion
134 $general_options = Metasync::get_option('general');
135
136 // Add JWT token if available for seamless login
137 if (isset($general_options['linkgraph_token']) && !empty($general_options['linkgraph_token'])) {
138 $dashboard_url .= '/?jwtToken=' . urlencode($general_options['linkgraph_token']);
139 }
140
141 // Add source tracking parameter
142 $dashboard_url .= (strpos($dashboard_url, '?') !== false ? '&' : '?') . 'source=wordpress-plugin';
143
144 // Add whitelabel identification if in whitelabel mode
145 $whitelabel_company_name = Metasync::get_whitelabel_company_name();
146 if ($whitelabel_company_name) {
147 $dashboard_url .= '&whitelabel=' . urlencode($whitelabel_company_name);
148 }
149
150 return $dashboard_url;
151 }
152
153 public const feature_sections = array(
154 'enable_404monitor' => 'Enable 404 Monitor',
155 'enable_siteverification' => 'Enable Site Verification',
156 'enable_localbusiness' => 'Enable Local Business',
157 'enable_codesnippets' => 'Enable Code Snippets',
158 'enable_googleconsole' => 'Enable Google Console',
159 'enable_optimalsettings' => 'Enable Optimal Settings',
160 'enable_globalsettings' => 'Enable Global Settings',
161 'enable_commonmetastatus' => 'Enable Common Meta Status',
162 'enable_socialmeta' => 'Enable Social Meta',
163 'enable_redirections' => 'Enable Redirections',
164 'enable_errorlogs' => 'Enable Error Logs'
165 );
166
167 private $database;
168 private $db_redirection;
169 public $db_heartbeat_errors;
170 public $setup_wizard;
171
172
173 /**
174 * Initialize the class and set its properties.
175 *
176 * @since 1.0.0
177 * @param string $plugin_name The name of this plugin.
178 * @param string $version The version of this plugin.
179 */
180
181 public function __construct($plugin_name, $version, &$database, $db_redirection, $db_heartbeat_errors) // , $data_error_log_list
182 {
183
184 $this->plugin_name = $plugin_name;
185 $this->version = $version;
186 $this->database = $database;
187 $this->db_redirection = $db_redirection;
188 $this->db_heartbeat_errors = $db_heartbeat_errors;
189 // $this->data_error_log_list = $data_error_log_list;
190
191 // Initialize setup wizard
192 $this->setup_wizard = new Metasync_Setup_Wizard($plugin_name, $version);
193
194 // Wire up the extracted settings-fields singleton
195 Metasync_Settings_Fields::instance()->set_admin_instance($this);
196
197 // Get data first for menu configuration
198 $data = Metasync::get_option('general');
199
200 // Set menu title using the effective title (includes whitelabel company name if available)
201 $this->menu_title = $this->get_effective_menu_title();
202 $raw_slug = isset($data['white_label_plugin_menu_slug']) ? $data['white_label_plugin_menu_slug'] : '';
203 $clean_slug = sanitize_title($raw_slug);
204 # Self-heal a legacy URL-shaped slug: persist the sanitized value so every
205 # reader that builds admin links from the stored option gets a valid WP menu slug.
206 if ($raw_slug !== '' && $clean_slug !== $raw_slug) {
207 $options = Metasync::get_option();
208 $options['general']['white_label_plugin_menu_slug'] = $clean_slug;
209 Metasync::set_option($options);
210 }
211 self::$page_slug = $clean_slug === '' ? 'searchatlas' : $clean_slug;
212
213 add_action('admin_menu', array($this, 'add_plugin_settings_page'));
214 add_action('admin_menu', array($this, 'add_import_external_data_page'));
215 add_action('admin_init', array($this, 'settings_page_init'));
216 add_filter('all_plugins', array($this,'metasync_plugin_white_label'));
217 add_filter( 'plugin_row_meta',array($this,'metasync_view_detials_url'),10,3);
218 add_filter('site_transient_update_plugins', array($this, 'inject_whitelabel_icon_into_update_transient'));
219
220 // Display transient error/success messages for redirections
221 add_action('admin_notices', array($this, 'display_redirection_messages'));
222
223 // Display CPU deferral notices when batch processing is deferred
224 add_action('admin_notices', array($this, 'display_cpu_deferral_notice'));
225
226 // Display LLMs.txt cross-plugin conflict notice
227 add_action('admin_notices', array($this, 'display_llms_txt_conflict_notice'));
228
229 // Add custom column for HTML-converted pages
230 add_filter('manage_posts_columns', array($this, 'add_html_converted_column'));
231 add_filter('manage_pages_columns', array($this, 'add_html_converted_column'));
232 add_action('manage_posts_custom_column', array($this, 'render_html_converted_column'), 10, 2);
233 add_action('manage_pages_custom_column', array($this, 'render_html_converted_column'), 10, 2);
234
235 // Add badge to page editor screen
236 add_action('edit_form_after_title', array($this, 'add_editor_source_notice'));
237
238 // Add badge to quick edit panel
239 add_action('quick_edit_custom_box', array($this, 'add_quick_edit_source_display'), 10, 2);
240
241 // Add dashboard widget
242 add_action('wp_dashboard_setup', array($this, 'add_html_pages_dashboard_widget'));
243
244 // Add Search Atlas status to WordPress admin bar (priority 999 to ensure plugin is fully loaded)
245 // Always add the action - the method will check the setting internally
246 add_action('admin_bar_menu', array($this, 'add_searchatlas_admin_bar_status'), 999);
247
248 #add css into admin header for icon image
249
250 add_action('admin_head', array($this,'metasync_admin_icon_style'));
251 add_action('admin_head', array($this, 'metasync_fouc_prevention_style'));
252 add_action('admin_head', array($this, 'suppress_notices_on_wizard_page'), 1);
253
254 // Always add admin bar styles - the method will check the setting internally
255 add_action('wp_head', array($this,'metasync_admin_bar_style')); // For frontend admin bar
256 add_action('admin_head', array($this,'metasync_admin_bar_style')); // For backend admin bar
257 // removing this as we don't need it anymore because we are using wp-ajax to implement the white label
258 // add_action('update_option_metasync_options', array($this, 'check_and_redirect_slug'), 10, 3);
259
260 // Sync plugin file headers whenever metasync_options is updated (covers AJAX save, Settings API, import)
261 add_action('update_option_metasync_options', array($this, 'on_options_updated_sync_file_headers'), 10, 2);
262
263 // Invalidate the admin bar status cache when settings change or the API key is rotated.
264 add_action('update_option_metasync_options', array('Metasync_Admin_Navigation', 'invalidate_admin_bar_status_cache'), 10, 0);
265 add_action('metasync_api_key_changed', array('Metasync_Admin_Navigation', 'invalidate_admin_bar_status_cache'), 10, 0);
266
267 add_action('admin_init', array($this, 'initialize_cookie'));
268 add_action('admin_init', array($this, 'maybe_redirect_to_wizard'));
269
270 // Add admin_post hooks for form submissions (WordPress standard way - no output buffering needed)
271 add_action('admin_post_metasync_clear_all_cache_plugins', array($this, 'handle_clear_all_cache_plugins'));
272 add_action('admin_post_metasync_clear_otto_cache_all', array($this, 'handle_clear_otto_cache_all'));
273 add_action('admin_post_metasync_clear_otto_cache_url', array($this, 'handle_clear_otto_cache_url'));
274 add_action('admin_post_metasync_purge_hosting_cache', array($this, 'handle_purge_hosting_cache'));
275
276 // Add AJAX for saving general settings
277 add_action( 'wp_ajax_meta_sync_save_settings', array($this,'meta_sync_save_settings') );
278
279 // Add AJAX for saving Indexation Control settings
280 add_action( 'wp_ajax_meta_sync_save_seo_controls', array($this,'meta_sync_save_seo_controls') );
281
282 // Add AJAX handler for saving Performance (CPU Load) settings
283 add_action('wp_ajax_metasync_save_performance_settings', array($this, 'ajax_save_performance_settings'));
284
285 // Add AJAX for saving execution settings
286 add_action( 'wp_ajax_metasync_save_execution_settings', array($this, 'ajax_save_execution_settings') );
287
288 // Add AJAX for saving hosting cache settings
289 add_action( 'wp_ajax_metasync_save_hosting_cache_settings', array($this, 'ajax_save_hosting_cache_settings') );
290
291 // Add AJAX for saving OTTO Cache TTL
292 add_action('wp_ajax_metasync_save_otto_cache_ttl', array($this, 'ajax_save_otto_cache_ttl'));
293 add_action( 'wp_ajax_metasync_save_object_cache_settings', array($this, 'ajax_save_object_cache_settings') );
294
295 // Add AJAX for saving edge cache / CDN settings
296 add_action( 'wp_ajax_metasync_save_edge_cache_settings', array('Metasync_Edge_Cache_Settings', 'ajax_save') );
297 // Add AJAX handler for Plugin Auth Token refresh
298 add_action('wp_ajax_metasync_refresh_plugin_auth_token', array($this, 'refresh_plugin_auth_token'));
299
300 // Add AJAX handler to get current Plugin Auth Token (for UI updates)
301 add_action('wp_ajax_metasync_get_plugin_auth_token', array($this, 'get_plugin_auth_token'));
302
303 // Add AJAX handler for creating redirects from 404 suggestions
304 add_action('wp_ajax_metasync_create_redirect_from_404', array($this, 'ajax_create_redirect_from_404'));
305
306 // Add AJAX handler for updating database structure
307 add_action('wp_ajax_metasync_update_db_structure', array($this, 'ajax_update_db_structure'));
308
309 // Add AJAX handlers for setup wizard
310 add_action('wp_ajax_metasync_save_wizard_progress', array($this, 'ajax_save_wizard_progress'));
311 add_action('wp_ajax_metasync_complete_wizard', array($this, 'ajax_complete_wizard'));
312
313 // Add AJAX handlers for robots.txt
314 add_action('wp_ajax_metasync_validate_robots', array($this, 'ajax_validate_robots'));
315 add_action('wp_ajax_metasync_get_default_robots', array($this, 'ajax_get_default_robots'));
316 add_action('wp_ajax_metasync_preview_robots_backup', array($this, 'ajax_preview_robots_backup'));
317 add_action('wp_ajax_metasync_delete_robots_backup', array($this, 'ajax_delete_robots_backup'));
318 add_action('wp_ajax_metasync_restore_robots_backup', array($this, 'ajax_restore_robots_backup'));
319
320 // Add AJAX handlers for host blocking test
321 add_action('wp_ajax_metasync_test_host_blocking_get', array($this, 'ajax_test_host_blocking_get'));
322 add_action('wp_ajax_metasync_test_host_blocking_post', array($this, 'ajax_test_host_blocking_post'));
323
324 // Add AJAX handlers for OTTO excluded URLs
325 add_action('wp_ajax_metasync_otto_add_excluded_url', array($this, 'ajax_otto_add_excluded_url'));
326 add_action('wp_ajax_metasync_otto_delete_excluded_url', array($this, 'ajax_otto_delete_excluded_url'));
327 add_action('wp_ajax_metasync_burst_ping', array($this, 'ajax_burst_ping'));
328 add_action('wp_ajax_metasync_otto_get_excluded_urls', array($this, 'ajax_otto_get_excluded_urls'));
329 add_action('wp_ajax_metasync_otto_recheck_excluded_url', array($this, 'ajax_otto_recheck_excluded_url'));
330
331 # Add AJAX handler for GA4 analytics tracking
332 add_action('wp_ajax_metasync_track_one_click_activation', array($this, 'ajax_track_one_click_activation'));
333
334 # Add AJAX handler for submitting issue reports
335 add_action('wp_ajax_metasync_submit_issue_report', array($this, 'ajax_submit_issue_report'));
336
337 # Add AJAX handlers for support token management
338
339 # Add AJAX handler for theme switcher
340 add_action('wp_ajax_metasync_save_theme', array($this, 'ajax_save_theme'));
341
342 # Add AJAX handlers for Sync Log management
343 add_action('wp_ajax_metasync_clear_sync_log', array($this, 'ajax_clear_sync_log'));
344 add_action('wp_ajax_metasync_rollback_mcp_change', array($this, 'ajax_rollback_mcp_change'));
345
346 # Add AJAX handler for external data import
347 add_action('wp_ajax_metasync_import_external_data', array($this, 'ajax_import_external_data'));
348
349 # Add AJAX handler for SEO metadata batch import
350 add_action('wp_ajax_metasync_import_seo_metadata', array($this, 'ajax_import_seo_metadata'));
351
352 # Add AJAX handler for password recovery
353 add_action('wp_ajax_metasync_recover_password', array($this, 'ajax_recover_password'));
354
355 # Add AJAX handler for resetting bot statistics
356 add_action('wp_ajax_metasync_reset_bot_stats', array($this, 'ajax_reset_bot_stats'));
357
358 # Add AJAX handlers for DB cleanup
359 add_action('wp_ajax_metasync_run_db_cleanup', array($this, 'ajax_run_db_cleanup'));
360 add_action('wp_ajax_metasync_save_db_cleanup_settings', array($this, 'ajax_save_db_cleanup_settings'));
361
362 # Add admin-post handler for exporting whitelabel settings (file download)
363 add_action('admin_post_metasync_export_whitelabel_settings', array($this, 'handle_export_whitelabel_settings'));
364
365 # Media Optimization AJAX handlers
366 add_action('wp_ajax_metasync_optimize_single_image', array($this, 'ajax_optimize_single_image'));
367 add_action('wp_ajax_metasync_revert_single_image', array($this, 'ajax_revert_single_image'));
368 add_action('wp_ajax_metasync_start_batch_optimize', array($this, 'ajax_start_batch_optimize'));
369 add_action('wp_ajax_metasync_cancel_batch_optimize', array($this, 'ajax_cancel_batch_optimize'));
370 add_action('wp_ajax_metasync_batch_progress', array($this, 'ajax_batch_progress'));
371 add_action('wp_ajax_metasync_bulk_optimize_selected', array($this, 'ajax_bulk_optimize_selected'));
372 add_action('wp_ajax_metasync_bulk_unoptimize_selected', array($this, 'ajax_bulk_unoptimize_selected'));
373 add_action('wp_ajax_metasync_process_batch_tick', array($this, 'ajax_process_batch_tick'));
374 add_action('wp_ajax_metasync_delete_orphaned_image', array($this, 'ajax_delete_orphaned_image'));
375 add_action('metasync_media_batch_optimize_cron', array($this, 'handle_media_batch_cron'));
376
377 # Add AJAX handlers for Google Instant Indexing
378 add_action('wp_ajax_metasync_send_giapi', array($this, 'ajax_send_giapi'));
379
380 # Add AJAX handlers for Bing Instant Indexing (IndexNow)
381 add_action('wp_ajax_metasync_send_bing_indexnow', array($this, 'ajax_send_bing_indexnow'));
382
383 # Add hooks for instant indexing settings saves
384 add_action('admin_init', array($this, 'save_instant_indexing_settings'));
385
386 # Add hooks for instant indexing post actions
387 add_filter('post_row_actions', array($this, 'add_instant_indexing_post_actions'), 10, 2);
388 add_filter('page_row_actions', array($this, 'add_instant_indexing_post_actions'), 10, 2);
389
390 # Add hooks for auto-submission on post publish
391 add_action('save_post', array($this, 'auto_submit_to_instant_indexing'), 10, 3);
392
393 // Add REST API endpoint for ping
394 add_action('rest_api_init', array($this, 'register_ping_rest_endpoint'));
395
396 // Add heartbeat cron functionality
397 add_filter('cron_schedules', array($this, 'add_heartbeat_cron_schedule'));
398 add_action('metasync_heartbeat_cron_check', array($this, 'execute_heartbeat_cron_check'));
399 add_action('metasync_burst_heartbeat', array($this, 'execute_burst_heartbeat'));
400 add_action('metasync_announce_cron', array($this, 'execute_announce_cron'));
401
402 // PR3: Transition to KEY_PENDING when API key is added or rotated
403 add_action('metasync_heartbeat_state_key_pending', array($this, 'set_heartbeat_state_key_pending'));
404
405 // Add transient cleanup cron functionality
406 add_action('metasync_cleanup_transients', array($this, 'execute_transient_cleanup'));
407
408 // Add DB cleanup cron functionality
409 add_action('metasync_db_cleanup', array($this, 'execute_db_cleanup'));
410
411 // Schedule heartbeat cron on plugin load (if not already scheduled)
412 add_action('init', array($this, 'maybe_schedule_heartbeat_cron'));
413
414 // Schedule transient cleanup cron on plugin load
415 add_action('init', array($this, 'maybe_schedule_transient_cleanup_cron'));
416
417 // Schedule DB cleanup cron on plugin load (only when enabled)
418 add_action('init', array($this, 'maybe_schedule_db_cleanup_cron'));
419
420 # Schedule hidden post manager cron on plugin load (runs every 7 days)
421 add_action('init', array($this, 'maybe_schedule_hidden_post_check'));
422
423 # Schedule OTTO 404 exclusion recheck (runs daily to recheck URLs excluded 7+ days ago)
424 add_action('init', array($this, 'maybe_schedule_otto_recheck_404_cron'));
425
426 # Schedule support token cleanup cron on plugin load (runs daily)
427
428 // Listen for immediate heartbeat trigger requests from other parts of the plugin
429 add_action('metasync_trigger_immediate_heartbeat', array($this, 'handle_immediate_heartbeat_trigger'));
430
431 // Listen for cron scheduling requests (after Search Atlas connect authentication)
432 add_action('metasync_ensure_heartbeat_cron_scheduled', array($this, 'maybe_schedule_heartbeat_cron'));
433
434 // Action Scheduler configuration filters
435 add_filter('action_scheduler_queue_runner_concurrent_batches', array($this, 'get_action_scheduler_batches'));
436 add_filter('action_scheduler_retention_period', array($this, 'get_action_scheduler_retention_period'));
437
438 // Note: Option change monitoring is now handled by the centralized API Key Monitor class
439 // This provides more comprehensive and intelligent monitoring of API key changes
440
441
442 #--------------------------
443 # we are disabling this code
444 # To prevent enabling debuging on every pluging Update
445 # This causes Issue #102 on gilab
446 #--------------------------
447 #
448 # NOTE:
449 # Do not delete this as we may need it in implementing universal logging
450
451 # add_action('upgrader_process_complete', array($this,'metasync_plugin_updated_action'), 10, 2);
452
453 # Hook into post category creation and update (AFTER they are saved)
454 add_action('saved_term', array($this,'admin_crud_term'), 10, 3);
455
456 # Hook into post category deletion (AFTER it is deleted)
457 // add_action('pre_delete_term', array($this,'admin_delete_term'), 10, 2);
458 # Using delete_category hook which fires AFTER category is fully deleted and provides correct data
459 add_action('delete_category', array($this,'admin_delete_category'), 10, 1);
460
461 }
462 /*
463 This function add css to wp admin header
464 */
465 public function metasync_admin_icon_style(){
466 # Get Metasync Option
467
468 $data= Metasync::get_option('general');
469
470 # Get white label menu slug
471 # Sanitize so a URL-shaped legacy value still yields a valid WP menu slug
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 if (!empty($video_opts['enabled'])) {
3274 if ($sitemap_generator->generate_video_sitemap()) {
3275 $extras[] = 'video';
3276 }
3277 }
3278
3279 // Generate main sitemap (its index will include news/video since they now exist)
3280 $result = $sitemap_generator->generate_sitemap();
3281
3282 if (is_wp_error($result)) {
3283 $error_msg = $result->get_error_message();
3284 error_log('[MetaSync] Sitemap generation failed: ' . $error_msg);
3285 echo '<div class="notice notice-error"><p>' . esc_html(
3286 sprintf(__('Sitemap generation failed: %s', 'metasync'), $error_msg)
3287 ) . '</p></div>';
3288 } else {
3289 $message = esc_html__('Sitemap generated successfully!', 'metasync');
3290 if (!empty($extras)) {
3291 $message .= ' ' . sprintf(
3292 esc_html__('Also generated %s sitemap(s).', 'metasync'),
3293 implode(' & ', $extras)
3294 );
3295 }
3296 if ($disabled_plugins) {
3297 $message .= ' ' . esc_html__('Conflicting sitemap generators have been automatically disabled.', 'metasync');
3298 }
3299
3300 // Check if robots.txt was updated
3301 $robots_result = get_transient('metasync_sitemap_robots_updated');
3302 if ($robots_result && $robots_result['success']) {
3303 if ($robots_result['action'] === 'added') {
3304 $message .= ' ' . esc_html__('Sitemap URL has been added to robots.txt.', 'metasync');
3305 } elseif ($robots_result['action'] === 'updated') {
3306 $message .= ' ' . esc_html__('Sitemap URL has been updated in robots.txt.', 'metasync');
3307 } elseif ($robots_result['action'] === 'created') {
3308 $message .= ' ' . esc_html__('robots.txt file has been created with sitemap URL.', 'metasync');
3309 }
3310 delete_transient('metasync_sitemap_robots_updated');
3311 }
3312
3313 echo '<div class="notice notice-success"><p>' . esc_html($message) . '</p></div>';
3314 }
3315 } elseif (isset($_POST['enable_auto_update'])) {
3316 update_option('metasync_sitemap_auto_update', true);
3317 $sitemap_generator->setup_auto_update_hooks();
3318 echo '<div class="notice notice-success"><p>' . esc_html__('Auto-update enabled!', 'metasync') . '</p></div>';
3319 } elseif (isset($_POST['disable_auto_update'])) {
3320 update_option('metasync_sitemap_auto_update', false);
3321 echo '<div class="notice notice-success"><p>' . esc_html__('Auto-update disabled!', 'metasync') . '</p></div>';
3322 } elseif (isset($_POST['delete_general_sitemap'])) {
3323 $deleted = $sitemap_generator->delete_sitemap('general');
3324
3325 if ($deleted) {
3326 // Disable auto-update only when the general sitemap is removed
3327 update_option('metasync_sitemap_auto_update', false);
3328 // Re-enable WP core sitemap only if no other MetaSync sitemaps remain
3329 if (!$sitemap_generator->sitemap_exists()) {
3330 delete_option('metasync_disable_wp_sitemap');
3331 }
3332 echo '<div class="notice notice-success"><p>' . esc_html__('General sitemap deleted successfully!', 'metasync') . '</p></div>';
3333 } else {
3334 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>';
3335 }
3336 } elseif (isset($_POST['delete_news_sitemap'])) {
3337 $deleted = $sitemap_generator->delete_sitemap('news');
3338
3339 if ($deleted) {
3340 echo '<div class="notice notice-success"><p>' . esc_html__('News sitemap deleted successfully!', 'metasync') . '</p></div>';
3341 } else {
3342 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>';
3343 }
3344 } elseif (isset($_POST['delete_video_sitemap'])) {
3345 $deleted = $sitemap_generator->delete_sitemap('video');
3346
3347 if ($deleted) {
3348 echo '<div class="notice notice-success"><p>' . esc_html__('Video sitemap deleted successfully!', 'metasync') . '</p></div>';
3349 } else {
3350 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>';
3351 }
3352 } elseif (isset($_POST['delete_sitemap'])) {
3353 // Delete all sitemaps: main + news + video (handled by delete_sitemap)
3354 $deleted = $sitemap_generator->delete_sitemap();
3355
3356 if ($deleted) {
3357 // Also disable auto-update when deleting
3358 update_option('metasync_sitemap_auto_update', false);
3359 // Re-enable WP core sitemap so the site isn't left with zero sitemaps
3360 delete_option('metasync_disable_wp_sitemap');
3361 echo '<div class="notice notice-success"><p>' . esc_html__('All sitemaps deleted successfully!', 'metasync') . '</p></div>';
3362 } else {
3363 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>';
3364 }
3365 } elseif (isset($_POST['enable_other_sitemaps'])) {
3366 // Re-enable other sitemap plugins
3367 $enabled_plugins = $sitemap_generator->enable_other_sitemap_generators();
3368 if ($enabled_plugins) {
3369 echo '<div class="notice notice-success"><p>' . esc_html__('Other sitemap plugins have been re-enabled successfully!', 'metasync') . '</p></div>';
3370 } else {
3371 echo '<div class="notice notice-info"><p>' . esc_html__('No sitemap plugins were found to re-enable.', 'metasync') . '</p></div>';
3372 }
3373 }
3374 }
3375
3376 // Get sitemap info
3377 $sitemap_exists = $sitemap_generator->sitemap_exists();
3378 $sitemap_url = $sitemap_generator->get_sitemap_url();
3379 $url_count = $sitemap_generator->count_urls();
3380 $last_generated = $sitemap_generator->get_last_generated_time();
3381 $auto_update_enabled = get_option('metasync_sitemap_auto_update', false);
3382 $active_sitemap_plugins = $sitemap_generator->check_active_sitemap_plugins();
3383
3384 // Main sitemap content settings
3385 $sitemap_settings = get_option('metasync_sitemap_settings', [
3386 'post_types' => [],
3387 'categories' => [],
3388 'tags' => [],
3389 'taxonomies' => [],
3390 'excluded_urls' => '',
3391 ]);
3392
3393 // News and video sitemap settings for tabs
3394 $news_settings = get_option('metasync_news_sitemap_settings', [
3395 'enabled' => false,
3396 'post_types' => ['post'],
3397 'categories' => [],
3398 'tags' => [],
3399 'taxonomies' => [],
3400 'excluded_urls' => '',
3401 'publication_name' => '',
3402 'publication_language' => '',
3403 ]);
3404 $video_settings = get_option('metasync_video_sitemap_settings', [
3405 'enabled' => false,
3406 'post_types' => ['post', 'page'],
3407 'auto_detect' => true,
3408 'taxonomies' => [],
3409 'excluded_urls' => '',
3410 ]);
3411
3412 // Load view
3413 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-xml-sitemap.php';
3414 }
3415
3416 /**
3417 * Custom Pages page callback
3418 */
3419 public function create_admin_custom_pages_page()
3420 {
3421 Metasync_Admin_Pages::get_instance($this)->create_admin_custom_pages_page();
3422 }
3423
3424 /**
3425 * 404 Monitor page callback
3426 */
3427 public function create_admin_404_monitor_page()
3428 {
3429 Metasync_Admin_Pages::get_instance($this)->create_admin_404_monitor_page();
3430 }
3431
3432 /**
3433 * SEO Health dashboard page callback
3434 */
3435 public function create_admin_seo_health_page()
3436 {
3437 require_once plugin_dir_path(__FILE__) . 'class-metasync-seo-health.php';
3438 Metasync_SEO_Health::get_instance()->render_page();
3439 }
3440
3441 /**
3442 * Site Verification page callback
3443 */
3444 public function create_admin_search_engine_verification_page()
3445 {
3446 Metasync_Admin_Pages::get_instance($this)->create_admin_search_engine_verification_page();
3447 }
3448
3449 /**
3450 * Local Business page callback
3451 */
3452 public function create_admin_local_business_page()
3453 {
3454 Metasync_Admin_Pages::get_instance($this)->create_admin_local_business_page();
3455 }
3456
3457 /**
3458 * Code Snippets page callback
3459 */
3460 public function create_admin_code_snippets_page()
3461 {
3462 Metasync_Admin_Pages::get_instance($this)->create_admin_code_snippets_page();
3463 }
3464
3465 /**
3466 * Schema Markup settings page callback
3467 */
3468 public function create_admin_schema_markup_page()
3469 {
3470 Metasync_Admin_Pages::get_instance($this)->create_admin_schema_markup_page();
3471 }
3472
3473 /**
3474 * Breadcrumbs settings page callback
3475 */
3476 public function create_admin_breadcrumbs_page()
3477 {
3478 Metasync_Admin_Pages::get_instance($this)->create_admin_breadcrumbs_page();
3479 }
3480
3481 /**
3482 * Google Instant Index Setting page callback
3483 */
3484 public function create_admin_google_instant_index_page()
3485 {
3486 $this->render_layout_open('Instant Indexing', 'instant_index', 'Submit URLs to Google for instant indexing via the Indexing API.');
3487
3488 // Render shared Google Index credentials section
3489 if (!function_exists('google_index_direct')) {
3490 if (file_exists(plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php')) {
3491 require_once plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php';
3492 } else {
3493 error_log('MetaSync Google Index: google-index-init.php not found at ' . plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php');
3494 return;
3495 }
3496 }
3497 $google_index = google_index_direct();
3498 $service_info = $google_index->get_service_account_info();
3499 $is_configured = !isset($service_info['error']);
3500
3501 $saved_json_display = $is_configured ? $google_index->get_redacted_config_json() : '';
3502
3503 include plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-index-api-settings.php';
3504
3505 // Render post types selection with save form
3506 $options = get_option('metasync_options_instant_indexing', ['post_types' => []]);
3507 $post_types_settings = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : [];
3508 ?>
3509 <form method="POST" action="">
3510 <?php include plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-instant-post-types.php'; ?>
3511 <div class="dashboard-card" style="padding: 20px;">
3512 <?php submit_button('Save Post Types', 'primary', 'submit', false, array('class' => 'button button-primary')); ?>
3513 </div>
3514 </form>
3515 <?php
3516
3517 $this->render_layout_close();
3518 }
3519
3520 /**
3521 * Google Console page callback
3522 */
3523 public function create_admin_google_console_page()
3524 {
3525 $this->render_layout_open('Google Console', 'google_console', 'View Google Search Console data and manage indexing requests.');
3526 $service_info = function_exists('google_index_direct') ? google_index_direct()->get_service_account_info() : ['error' => 'Module not loaded'];
3527 $is_configured = !isset($service_info['error']);
3528 include_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-console.php';
3529 $this->render_layout_close();
3530 }
3531
3532 /**
3533 * Bing Console page callback
3534 */
3535 public function create_admin_bing_console_page()
3536 {
3537 $this->render_layout_open('Bing Console', 'bing_console', 'Submit URLs to Bing for instant indexing via IndexNow.');
3538 require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php';
3539 $bing_instant_index = new Metasync_Bing_Instant_Index();
3540 $bing_instant_index->show_bing_instant_indexing_console();
3541 $this->render_layout_close();
3542 }
3543
3544 /**
3545 * General Options page callback
3546 */
3547 public function create_admin_optimal_settings_page()
3548 {
3549 Metasync_Admin_Pages::get_instance($this)->create_admin_optimal_settings_page();
3550 }
3551
3552 /**
3553 * Global Options page callback
3554 */
3555 public function create_admin_global_settings_page()
3556 {
3557 Metasync_Admin_Pages::get_instance($this)->create_admin_global_settings_page();
3558 }
3559
3560 /**
3561 * Common Meta Options page callback
3562 */
3563 public function create_admin_common_meta_settings_page()
3564 {
3565 Metasync_Admin_Pages::get_instance($this)->create_admin_common_meta_settings_page();
3566 }
3567
3568 /**
3569 * Social meta page callback
3570 */
3571 public function create_admin_social_meta_page()
3572 {
3573 Metasync_Admin_Pages::get_instance($this)->create_admin_social_meta_page();
3574 }
3575
3576
3577 /**
3578 * Indexation Control page callback
3579 */
3580 public function create_admin_seo_controls_page()
3581 {
3582 Metasync_Admin_Pages::get_instance($this)->create_admin_seo_controls_page();
3583 }
3584
3585 /**
3586 * Site Optimal Settings page callback
3587 */
3588 public function optimization_settings_options()
3589 {
3590 Metasync_Admin_Pages::get_instance($this)->optimization_settings_options();
3591 }
3592
3593 /**
3594 * redirection page callback with tabs
3595 */
3596 public function create_admin_redirections_page()
3597 {
3598 Metasync_Redirections_Admin::get_instance($this->db_redirection, $this)->create_admin_redirections_page();
3599 }
3600
3601 /**
3602 * Display transient error/success messages for redirections
3603 */
3604 public function display_redirection_messages()
3605 {
3606 Metasync_Redirections_Admin::get_instance($this->db_redirection, $this)->display_redirection_messages();
3607 }
3608
3609 /**
3610 * Display admin notice when batch processing was deferred due to high CPU load.
3611 * Reads transient set by Metasync_CPU_Monitor::record_deferral() and clears it.
3612 */
3613 public function display_cpu_deferral_notice()
3614 {
3615 $data = get_transient( Metasync_CPU_Monitor::DEFER_NOTICE_TRANSIENT );
3616 if ( ! $data || ! is_array( $data ) ) {
3617 return;
3618 }
3619 delete_transient( Metasync_CPU_Monitor::DEFER_NOTICE_TRANSIENT );
3620 echo '<div class="notice notice-warning is-dismissible"><p>';
3621 printf(
3622 /* translators: 1: plugin name, 2: current load, 3: threshold, 4: core count */
3623 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' ),
3624 esc_html( Metasync::get_effective_plugin_name() ),
3625 esc_html( $data['load'] ),
3626 esc_html( $data['threshold'] ),
3627 esc_html( $data['cores'] )
3628 );
3629 echo '</p></div>';
3630 }
3631
3632 /**
3633 * Display info notice when another SEO plugin also generates /llms.txt.
3634 *
3635 * MetaSync always serves its own version when enabled (priority 1). This
3636 * notice simply informs the admin that another plugin was detected.
3637 */
3638 public function display_llms_txt_conflict_notice()
3639 {
3640 if (!get_transient('metasync_llms_conflict')) {
3641 return;
3642 }
3643 echo '<div class="notice notice-info is-dismissible"><p>';
3644 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()));
3645 echo '</p></div>';
3646 }
3647
3648 /**
3649 * AJAX handler for updating database structure
3650 */
3651 public function ajax_update_db_structure()
3652 {
3653 Metasync_Admin_Ajax::instance()->ajax_update_db_structure();
3654 }
3655
3656 /**
3657 * AJAX handler to save wizard progress
3658 *
3659 * @since 1.0.0
3660 */
3661 public function ajax_save_wizard_progress()
3662 {
3663 Metasync_Admin_Ajax::instance()->ajax_save_wizard_progress();
3664 }
3665
3666 /**
3667 * AJAX handler to complete wizard
3668 *
3669 * @since 1.0.0
3670 */
3671 public function ajax_complete_wizard()
3672 {
3673 Metasync_Admin_Ajax::instance()->ajax_complete_wizard();
3674 }
3675
3676 /**
3677 * AJAX handler to validate robots.txt content
3678 */
3679 public function ajax_validate_robots()
3680 {
3681 Metasync_Admin_Ajax::instance()->ajax_validate_robots();
3682 }
3683
3684 /**
3685 * AJAX handler to get default robots.txt content
3686 */
3687 public function ajax_get_default_robots()
3688 {
3689 Metasync_Admin_Ajax::instance()->ajax_get_default_robots();
3690 }
3691
3692 /**
3693 * AJAX handler to preview robots.txt backup content
3694 */
3695 public function ajax_preview_robots_backup()
3696 {
3697 Metasync_Admin_Ajax::instance()->ajax_preview_robots_backup();
3698 }
3699
3700 /**
3701 * AJAX handler to delete robots.txt backup
3702 */
3703 public function ajax_delete_robots_backup()
3704 {
3705 Metasync_Admin_Ajax::instance()->ajax_delete_robots_backup();
3706 }
3707
3708 /**
3709 * AJAX handler to restore robots.txt backup
3710 */
3711 public function ajax_restore_robots_backup()
3712 {
3713 Metasync_Admin_Ajax::instance()->ajax_restore_robots_backup();
3714 }
3715 public function ajax_create_redirect_from_404()
3716 {
3717 Metasync_Admin_Ajax::instance()->ajax_create_redirect_from_404();
3718 }
3719
3720 /**
3721 * AJAX handler for testing host blocking with GET request
3722 */
3723 public function ajax_test_host_blocking_get()
3724 {
3725 Metasync_Admin_Ajax::instance()->ajax_test_host_blocking_get();
3726 }
3727
3728 /**
3729 * AJAX handler for testing host blocking with POST request
3730 */
3731 public function ajax_test_host_blocking_post()
3732 {
3733 Metasync_Admin_Ajax::instance()->ajax_test_host_blocking_post();
3734 }
3735
3736 /**
3737 * Register REST API endpoint for ping
3738 */
3739 public function register_ping_rest_endpoint()
3740 {
3741 register_rest_route('metasync/v1', '/ping', array(
3742 'methods' => array('GET', 'POST'),
3743 'callback' => array($this, 'handle_ping_rest_endpoint'),
3744 'permission_callback' => '__return_true', // Allow public access
3745 'args' => array(
3746 'test' => array(
3747 'description' => 'Optional test parameter',
3748 'type' => 'string',
3749 'sanitize_callback' => 'sanitize_text_field',
3750 ),
3751 ),
3752 ));
3753 }
3754
3755 /**
3756 * Handle REST API ping endpoint
3757 */
3758 public function handle_ping_rest_endpoint($request)
3759 {
3760 // Get request method
3761 $method = $request->get_method();
3762
3763 // Prepare response data
3764 $response_data = array(
3765 'response' => 'pong',
3766 'method' => $method,
3767 'timestamp' => current_time('mysql'),
3768 'site_url' => home_url(),
3769 'plugin_version' => METASYNC_VERSION
3770 );
3771
3772 // Add request data for POST requests
3773 if ($method === 'POST') {
3774 $body = $request->get_body();
3775 if (!empty($body)) {
3776 $response_data['received_data'] = json_decode($body, true);
3777 }
3778
3779 // Add any query parameters
3780 $params = $request->get_params();
3781 if (!empty($params)) {
3782 $response_data['query_params'] = $params;
3783 }
3784 }
3785
3786 // Add test parameter if provided
3787 $test_param = $request->get_param('test');
3788 if (!empty($test_param)) {
3789 $response_data['test_param'] = $test_param;
3790 }
3791
3792 return new WP_REST_Response($response_data, 200);
3793 }
3794
3795 /**
3796 * Site error logs page callback
3797 */
3798
3799 public function create_admin_error_logs_page()
3800 {
3801 Metasync_Admin_Pages::get_instance($this)->create_admin_error_logs_page();
3802 }
3803
3804 /**
3805 * Compatibility page callback
3806 */
3807 public function create_admin_compatibility_page()
3808 {
3809 Metasync_Compatibility_Checker::instance()->create_admin_compatibility_page($this);
3810 }
3811
3812 /**
3813 * Sync Log page callback
3814 */
3815 public function create_admin_sync_log_page()
3816 {
3817 // Classes are now autoloaded
3818
3819 $sync_db = new Metasync_Sync_History_Database();
3820
3821 // Handle AJAX requests for sync log data
3822 if (wp_doing_ajax()) {
3823 $this->handle_sync_log_ajax();
3824 return;
3825 }
3826
3827 // Get pagination parameters
3828 $page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
3829 $per_page = 10;
3830 $offset = ($page - 1) * $per_page;
3831
3832 // Get filters
3833 $filters = [
3834 // UI exposes date_range and status only. We compute date_from/date_to based on date_range
3835 'date_range' => isset($_GET['date_range']) ? sanitize_text_field(wp_unslash($_GET['date_range'])) : '',
3836 'status' => isset($_GET['status']) ? sanitize_text_field(wp_unslash($_GET['status'])) : '',
3837 ];
3838
3839 // Map date_range to concrete date_from/date_to for DB queries
3840 $date_range = $filters['date_range'];
3841 $wp_now_ts = current_time('timestamp');
3842 $date_from = '';
3843 $date_to = '';
3844
3845 if (!empty($date_range)) {
3846 // End boundary is now by default
3847 $date_to = date('Y-m-d H:i:s', $wp_now_ts);
3848
3849 if ($date_range === 'today') {
3850 $start_ts = strtotime('today', $wp_now_ts);
3851 $date_from = date('Y-m-d H:i:s', $start_ts);
3852 } elseif ($date_range === 'yesterday') {
3853 $start_ts = strtotime('yesterday', $wp_now_ts);
3854 $end_ts = strtotime('today', $wp_now_ts) - 1; // end of yesterday
3855 $date_from = date('Y-m-d H:i:s', $start_ts);
3856 $date_to = date('Y-m-d H:i:s', $end_ts);
3857 } elseif ($date_range === 'this_week') {
3858 $start_of_week = (int) get_option('start_of_week', 1); // 0=Sun, 1=Mon
3859 $day_of_week = (int) date('w', $wp_now_ts); // 0=Sun..6=Sat
3860 // Convert start_of_week to PHP's 0..6 where 0=Sunday
3861 $delta_days = ($day_of_week - $start_of_week + 7) % 7;
3862 $start_ts = strtotime('-' . $delta_days . ' days', strtotime('today', $wp_now_ts));
3863 $date_from = date('Y-m-d H:i:s', $start_ts);
3864 } elseif ($date_range === 'this_month') {
3865 $start_ts = strtotime(date('Y-m-01 00:00:00', $wp_now_ts));
3866 $date_from = date('Y-m-d H:i:s', $start_ts);
3867 } elseif ($date_range === 'all') {
3868 // no bounds
3869 }
3870 }
3871
3872 if (!empty($date_from)) {
3873 $filters['date_from'] = $date_from;
3874 }
3875 if (!empty($date_to)) {
3876 $filters['date_to'] = $date_to;
3877 }
3878
3879 // Remove empty filters
3880 $filters = array_filter($filters);
3881
3882 // Get sync history records
3883 $sync_records = $sync_db->getAllRecords($per_page, $offset, $filters);
3884 $total_records = $sync_db->get_count($filters);
3885 $total_pages = ceil($total_records / $per_page);
3886
3887 // Get statistics
3888 $stats = $sync_db->get_statistics();
3889
3890 $this->render_layout_open('Changes Log', 'sync_log', 'Track recent content synchronizations and changes from external tools.');
3891 ?>
3892 <div class="dashboard-card">
3893 <div class="sync-log-header">
3894 <div class="sync-log-title-section">
3895 <h2>Changes Log</h2>
3896 <p style="color: var(--dashboard-text-secondary); margin-bottom: 0;">
3897 Recent content synchronizations from external tools.
3898 <span style="margin-left:8px; font-size:12px; opacity:.75;">Records are automatically removed after 90 days.</span>
3899 </p>
3900 </div>
3901
3902 <!-- Filters + Clear Log button - Right aligned -->
3903 <div class="sync-log-filters" style="display:flex;align-items:center;gap:10px;">
3904 <button type="button" id="metasync-clear-sync-log-btn"
3905 style="background:#dc3545;color:#fff;border:none;padding:6px 14px;border-radius:4px;cursor:pointer;font-size:13px;"
3906 data-nonce="<?php echo esc_attr(wp_create_nonce('metasync_clear_sync_log')); ?>">
3907 🗑 Clear Log
3908 </button>
3909 <form method="get" class="sync-filters-form" onchange="this.submit()" style="display:flex;flex-direction:row;align-items:center;gap:12px;flex-wrap:nowrap;">
3910 <input type="hidden" name="page" value="<?php echo esc_attr($_GET['page']); ?>">
3911
3912 <select name="date_range" class="sync-filter-select">
3913 <option value="all" <?php selected($filters['date_range'] ?? 'all', 'all'); ?>> All Time</option>
3914 <option value="today" <?php selected($filters['date_range'] ?? '', 'today'); ?>>Today</option>
3915 <option value="yesterday" <?php selected($filters['date_range'] ?? '', 'yesterday'); ?>>Yesterday</option>
3916 <option value="this_week" <?php selected($filters['date_range'] ?? '', 'this_week'); ?>>This week</option>
3917 <option value="this_month" <?php selected($filters['date_range'] ?? '', 'this_month'); ?>>This month</option>
3918 </select>
3919
3920 <select name="status" class="sync-filter-select">
3921 <option value="" <?php selected($filters['status'] ?? '', ''); ?>>Status Filter</option>
3922 <option value="published" <?php selected($filters['status'] ?? '', 'published'); ?>>Published</option>
3923 <option value="draft" <?php selected($filters['status'] ?? '', 'draft'); ?>>Draft</option>
3924 </select>
3925 </form>
3926 </div>
3927 </div>
3928
3929 <!-- Sync History List -->
3930 <div class="sync-log-list">
3931 <?php if (empty($sync_records)): ?>
3932 <div class="sync-log-empty">
3933 <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>
3934 <h3>No sync records found</h3>
3935 <p>Sync records will appear here when content/pages receive new updates.</p>
3936 </div>
3937 <?php else: ?>
3938 <?php foreach ($sync_records as $record): ?>
3939 <div class="sync-log-item">
3940 <div class="sync-log-icon">
3941 <div class="sync-icon-circle">
3942 <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>
3943 </div>
3944 </div>
3945
3946 <div class="sync-log-content">
3947 <div class="sync-log-title"><?php echo esc_html($record->title); ?>
3948 <?php if (!empty($record->url)): ?>
3949 <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>
3950 <?php endif; ?>
3951 </div>
3952 <div class="sync-log-meta">
3953 <?php echo esc_html( $this->time_elapsed_string($record->created_at) ); ?>
3954 <?php if (!empty($record->source)): ?>
3955 &nbsp;·&nbsp;<span style="opacity:.7;"><?php echo esc_html($record->source); ?></span>
3956 <?php endif; ?>
3957 </div>
3958 </div>
3959
3960 <div class="sync-log-status" style="display:flex;align-items:center;gap:8px;">
3961 <?php
3962 $st = (string) $record->status;
3963 $b_label = ucfirst($st); $b_bg = '#6b7280'; $b_icon = 'dashicons-info-outline';
3964 if ($st === 'published' || $st === 'publish' || $st === 'success' || $st === 'partial') { $b_label = 'Published'; $b_bg = '#16a34a'; $b_icon = 'dashicons-yes'; }
3965 elseif ($st === 'updated') { $b_label = 'Updated'; $b_bg = '#0d9488'; $b_icon = 'dashicons-update'; }
3966 elseif ($st === 'failed' || $st === 'conflict' || $st === 'locked') { $b_label = 'Not imported'; $b_bg = '#64748b'; $b_icon = 'dashicons-minus'; }
3967 elseif ($st === 'draft') { $b_label = 'Draft'; $b_bg = '#6b7280'; $b_icon = 'dashicons-info-outline'; }
3968 ?>
3969 <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;">
3970 <span class="dashicons <?php echo esc_attr($b_icon); ?>" style="font-size:14px;width:14px;height:14px;line-height:14px;"></span>
3971 <?php echo esc_html($b_label); ?>
3972 </span>
3973 <?php if ($record->source === 'MCP Client'): ?>
3974 <button type="button"
3975 class="metasync-rollback-btn"
3976 data-id="<?php echo esc_attr($record->id); ?>"
3977 data-nonce="<?php echo esc_attr(wp_create_nonce('metasync_rollback_mcp_change')); ?>"
3978 style="background:none;border:1px solid #aaa;border-radius:4px;padding:3px 8px;cursor:pointer;font-size:12px;color:inherit;"
3979 title="Rollback this MCP change">
3980 Rollback
3981 </button>
3982 <?php endif; ?>
3983 </div>
3984 </div>
3985 <?php endforeach; ?>
3986 <?php endif; ?>
3987 </div>
3988
3989 <!-- Pagination -->
3990 <?php if ($total_pages > 1): ?>
3991 <div class="sync-log-pagination">
3992 <div class="sync-log-pagination-info">
3993 Total records: <?php echo intval( $total_records ); ?> | Showing <?php echo intval( $offset ) + 1; ?>-<?php echo intval( min($offset + $per_page, $total_records) ); ?>
3994 </div>
3995
3996 <div class="sync-log-pagination-controls">
3997 <?php if ($page > 1): ?>
3998 <a href="?page=<?php echo esc_attr($_GET['page']); ?>&paged=<?php echo intval( $page ) - 1; ?><?php echo esc_html( $this->build_filter_query_string($filters) ); ?>" class="sync-pagination-btn"></a>
3999 <?php endif; ?>
4000
4001 <?php for ($i = max(1, $page - 2); $i <= min($total_pages, $page + 2); $i++): ?>
4002 <a href="?page=<?php echo esc_attr($_GET['page']); ?>&paged=<?php echo intval( $i ); ?><?php echo esc_html( $this->build_filter_query_string($filters) ); ?>"
4003 class="sync-pagination-btn <?php echo $i === $page ? 'active' : ''; ?>"><?php echo intval( $i ); ?></a>
4004 <?php endfor; ?>
4005
4006 <?php if ($page < $total_pages): ?>
4007 <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>
4008 <?php endif; ?>
4009 </div>
4010 </div>
4011 <?php endif; ?>
4012 </div>
4013 <?php $this->render_layout_close(); ?>
4014
4015 <script>
4016 (function () {
4017 // ── Clear Log ──────────────────────────────────────────────
4018 var clearBtn = document.getElementById('metasync-clear-sync-log-btn');
4019 if (clearBtn) {
4020 clearBtn.addEventListener('click', function () {
4021 if (!confirm('Are you sure you want to permanently delete all sync log records? This cannot be undone.')) {
4022 return;
4023 }
4024 clearBtn.disabled = true;
4025 clearBtn.textContent = 'Clearing…';
4026 var data = new FormData();
4027 data.append('action', 'metasync_clear_sync_log');
4028 data.append('nonce', clearBtn.dataset.nonce);
4029 fetch(ajaxurl, { method: 'POST', body: data })
4030 .then(function (r) { return r.json(); })
4031 .then(function (resp) {
4032 if (resp.success) {
4033 window.location.reload();
4034 } else {
4035 alert(resp.data && resp.data.message ? resp.data.message : 'Failed to clear log.');
4036 clearBtn.disabled = false;
4037 clearBtn.textContent = '🗑 Clear Log';
4038 }
4039 })
4040 .catch(function () {
4041 alert('Request failed. Please try again.');
4042 clearBtn.disabled = false;
4043 clearBtn.textContent = '🗑 Clear Log';
4044 });
4045 });
4046 }
4047
4048 // ── Rollback ───────────────────────────────────────────────
4049 document.querySelectorAll('.metasync-rollback-btn').forEach(function (btn) {
4050 btn.addEventListener('click', function () {
4051 if (!confirm('Rollback this MCP change to its previous state?')) {
4052 return;
4053 }
4054 btn.disabled = true;
4055 btn.textContent = '';
4056 var data = new FormData();
4057 data.append('action', 'metasync_rollback_mcp_change');
4058 data.append('nonce', btn.dataset.nonce);
4059 data.append('sync_history_id', btn.dataset.id);
4060 fetch(ajaxurl, { method: 'POST', body: data })
4061 .then(function (r) { return r.json(); })
4062 .then(function (resp) {
4063 if (resp.success) {
4064 btn.textContent = '✓ Done';
4065 btn.style.color = 'green';
4066 } else {
4067 alert(resp.data && resp.data.message ? resp.data.message : 'Rollback failed.');
4068 btn.disabled = false;
4069 btn.textContent = '↩ Rollback';
4070 }
4071 })
4072 .catch(function () {
4073 alert('Request failed. Please try again.');
4074 btn.disabled = false;
4075 btn.textContent = '↩ Rollback';
4076 });
4077 });
4078 });
4079 })();
4080 </script>
4081 <?php
4082 }
4083 /**
4084 * Build filter query string for pagination
4085 */
4086 private function build_filter_query_string($filters)
4087 {
4088 $query_parts = [];
4089 foreach ($filters as $key => $value) {
4090 if (!empty($value)) {
4091 $query_parts[] = $key . '=' . urlencode($value);
4092 }
4093 }
4094 return !empty($query_parts) ? '&' . implode('&', $query_parts) : '';
4095 }
4096
4097 /**
4098 * Handle AJAX requests for sync log data
4099 */
4100 private function handle_sync_log_ajax()
4101 {
4102 // This can be used for future AJAX functionality like real-time updates
4103 wp_die();
4104 }
4105
4106 /**
4107 * AJAX: Clear all Sync Log records (admin-only, nonce protected).
4108 */
4109 public function ajax_clear_sync_log()
4110 {
4111 check_ajax_referer('metasync_clear_sync_log', 'nonce');
4112
4113 if (!current_user_can('manage_options')) {
4114 wp_send_json_error(['message' => 'Insufficient permissions.'], 403);
4115 }
4116
4117 $sync_db = new Metasync_Sync_History_Database();
4118 $sync_db->clear_logs();
4119
4120 wp_send_json_success(['message' => 'Sync log cleared successfully.']);
4121 }
4122
4123 /**
4124 * AJAX: Rollback a single MCP Client sync history entry.
4125 */
4126 public function ajax_rollback_mcp_change()
4127 {
4128 check_ajax_referer('metasync_rollback_mcp_change', 'nonce');
4129
4130 if (!current_user_can('manage_options')) {
4131 wp_send_json_error(['message' => 'Insufficient permissions.'], 403);
4132 }
4133
4134 $id = isset($_POST['sync_history_id']) ? intval($_POST['sync_history_id']) : 0;
4135 if (!$id) {
4136 wp_send_json_error(['message' => 'Invalid sync history ID.']);
4137 }
4138
4139 $result = Metasync_MCP_Sync_Logger::rollback($id);
4140
4141 if ($result['success']) {
4142 wp_send_json_success(['message' => $result['message']]);
4143 } else {
4144 wp_send_json_error(['message' => $result['message']]);
4145 }
4146 }
4147
4148 /**
4149 * Render compatibility sections
4150 */
4151 private function render_compatibility_sections()
4152 {
4153 Metasync_Compatibility_Checker::instance()->render_compatibility_sections();
4154 }
4155
4156 /**
4157 * Render Page Builders section
4158 */
4159 private function render_page_builders_section()
4160 {
4161 Metasync_Compatibility_Checker::instance()->render_page_builders_section();
4162 }
4163
4164 /**
4165 * Render SEO Plugins section
4166 */
4167 private function render_seo_plugins_section()
4168 {
4169 Metasync_Compatibility_Checker::instance()->render_seo_plugins_section();
4170 }
4171
4172 /**
4173 * Render Cache Plugins section
4174 */
4175 private function render_cache_plugins_section()
4176 {
4177 Metasync_Compatibility_Checker::instance()->render_cache_plugins_section();
4178 }
4179
4180 /**
4181 * Render Lock Section button for protected tabs
4182 *
4183 * @param string $tab The tab identifier (general, whitelabel, advanced)
4184 */
4185 private function render_lock_button($tab)
4186 {
4187 Metasync_Compatibility_Checker::instance()->render_lock_button($tab);
4188 }
4189
4190 /**
4191 * Get Page Builders compatibility information
4192 */
4193 private function get_page_builders_compatibility()
4194 {
4195 return Metasync_Compatibility_Checker::instance()->get_page_builders_compatibility();
4196 }
4197
4198 /**
4199 * Get SEO Plugins compatibility information
4200 */
4201 private function get_seo_plugins_compatibility()
4202 {
4203 return Metasync_Compatibility_Checker::instance()->get_seo_plugins_compatibility();
4204 }
4205
4206 /**
4207 * Get Cache Plugins compatibility information
4208 */
4209 private function get_cache_plugins_compatibility()
4210 {
4211 return Metasync_Compatibility_Checker::instance()->get_cache_plugins_compatibility();
4212 }
4213
4214 /**
4215 * Check if a plugin is installed and active
4216 * @deprecated Use get_plugin_status() instead
4217 */
4218 private function is_plugin_installed($plugin_file)
4219 {
4220 return Metasync_Compatibility_Checker::instance()->is_plugin_installed($plugin_file);
4221 }
4222
4223 /**
4224 * Get detailed plugin status (installed and/or active)
4225 * Checks multiple plugin file paths (e.g., free and premium versions)
4226 *
4227 * @param array $plugin_files Array of plugin file paths to check (e.g., ['free/plugin.php', 'pro/plugin.php'])
4228 * @param bool $is_core Whether this is a WordPress core feature (always installed/active)
4229 * @param string $theme_name Optional theme name to check if it's a theme instead of plugin
4230 * @return array ['is_installed' => bool, 'is_active' => bool, 'active_version' => string|null]
4231 */
4232 private function get_plugin_status($plugin_files, $is_core = false, $theme_name = null)
4233 {
4234 return Metasync_Compatibility_Checker::instance()->get_plugin_status($plugin_files, $is_core, $theme_name);
4235 }
4236
4237
4238 /**
4239 * Get plugin logo URL (optimized for performance)
4240 */
4241 private function get_plugin_logo($plugin_key, $type)
4242 {
4243 return Metasync_Compatibility_Checker::instance()->get_plugin_logo($plugin_key, $type);
4244 }
4245
4246
4247 public function creat_error_Logs_List()
4248 {
4249 Metasync_Admin_Pages::get_instance($this)->creat_error_Logs_List();
4250 }
4251
4252 /**
4253 * Site error logs page callback
4254 */
4255 public function create_admin_heartbeat_error_logs_page()
4256 {
4257 Metasync_Admin_Pages::get_instance($this)->create_admin_heartbeat_error_logs_page();
4258 }
4259
4260
4261 /**
4262 * Handle session management early for whitelabel functionality
4263 */
4264 private function handle_session_management_early()
4265 {
4266 Metasync_Connect_Manager::instance()->handle_session_management_early();
4267 }
4268
4269 /**
4270 * @deprecated 2.5.12 Use Metasync_Auth_Manager instead of sessions for authentication
4271 */
4272 private function safe_session_start() {
4273 // This method is deprecated and no longer used
4274 // Authentication now uses Metasync_Auth_Manager with WordPress transients and user meta
4275 _deprecated_function(__METHOD__, '2.5.12', 'Metasync_Auth_Manager');
4276 return Metasync_Session_Helper::safe_start();
4277 }
4278
4279 private function handle_whitelabel_session_logic()
4280 {
4281 Metasync_Connect_Manager::instance()->handle_whitelabel_session_logic();
4282 }
4283
4284 private function handle_whitelabel_password_early()
4285 {
4286 Metasync_Connect_Manager::instance()->handle_whitelabel_password_early();
4287 }
4288
4289 /**
4290 * Get accordion sections configuration for General Settings
4291 *
4292 * @return array Accordion sections with field IDs, icons, and descriptions
4293 */
4294 private function get_accordion_sections_config() {
4295 return Metasync_Settings_Fields::instance()->get_accordion_sections_config();
4296 }
4297
4298 /**
4299 * Get accordion sections configuration for Advanced Settings Tab
4300 *
4301 * @return array Accordion sections configuration
4302 */
4303 private function get_advanced_accordion_config() {
4304 return Metasync_Settings_Fields::instance()->get_advanced_accordion_config();
4305 }
4306
4307 /**
4308 * Render accordion sections for Advanced Settings Tab
4309 */
4310 public function render_advanced_accordion() {
4311 Metasync_Settings_Fields::instance()->render_advanced_accordion();
4312 }
4313
4314 /**
4315 * Render reset settings section for Advanced tab accordion
4316 */
4317 /**
4318 * Render CPU Monitor section for Performance accordion
4319 */
4320 public function render_cpu_monitor_section() {
4321 $cpu_monitor = new Metasync_CPU_Monitor();
4322 $stats = Metasync_CPU_Monitor::get_stats();
4323 $per_core_threshold = Metasync_CPU_Monitor::get_per_core_threshold();
4324 $cores = Metasync_CPU_Monitor::get_cpu_core_count();
4325 $effective_threshold = Metasync_CPU_Monitor::get_effective_threshold();
4326 $detection_reliable = Metasync_CPU_Monitor::is_core_detection_reliable();
4327 ?>
4328 <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px;">
4329 <!-- CPU Cores Detected -->
4330 <div style="margin-bottom: 24px;">
4331 <label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
4332 CPU Cores Detected
4333 </label>
4334 <div style="padding: 10px 12px; background: var(--dashboard-input-bg); border: 1px solid var(--dashboard-border); border-radius: 6px; color: var(--dashboard-text-secondary);">
4335 <?php if ($detection_reliable) : ?>
4336 <strong><?php echo intval($cores); ?></strong> core<?php echo $cores !== 1 ? 's' : ''; ?>
4337 <?php else : ?>
4338 <strong>Not detected</strong>
4339 <?php endif; ?>
4340 </div>
4341 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
4342 <?php if ($detection_reliable) : ?>
4343 Automatically detected on this system.
4344 <?php else : ?>
4345 Core detection is not available on this hosting environment.
4346 <?php endif; ?>
4347 </p>
4348 </div>
4349
4350 <!-- Per-Core Load Threshold -->
4351 <div style="margin-bottom: 24px;">
4352 <label for="cpu_load_per_core_threshold" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
4353 Per-Core Load Threshold
4354 <?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.'); ?>
4355 </label>
4356 <input type="number"
4357 id="cpu_load_per_core_threshold"
4358 name="metasync_options[performance][cpu_load_per_core_threshold]"
4359 value="<?php echo esc_attr($per_core_threshold); ?>"
4360 step="0.1"
4361 min="0.5"
4362 max="10.0"
4363 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;"
4364 onchange="updateEffectiveThreshold()">
4365 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
4366 Set the load average per CPU core (0.5–10.0). Default: 2.0
4367 </p>
4368 </div>
4369
4370 <!-- Effective Threshold (Read-Only) -->
4371 <div style="margin-bottom: 24px;">
4372 <label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
4373 Effective Threshold
4374 </label>
4375 <div style="padding: 10px 12px; background: var(--dashboard-input-bg); border: 1px solid var(--dashboard-border); border-radius: 6px; color: var(--dashboard-text-secondary);">
4376 <strong id="effective_threshold_value"><?php echo round($effective_threshold, 2); ?></strong>
4377 </div>
4378 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
4379 Calculated as: cores × per-core threshold
4380 </p>
4381 </div>
4382
4383 <!-- Statistics -->
4384 <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;">
4385 <h4 style="margin: 0 0 12px 0; color: var(--dashboard-text); display: flex; align-items: center; gap: 8px;">
4386 <span class="dashicons dashicons-chart-bar" style="font-size:18px;width:18px;height:18px;"></span>
4387 <span>CPU Load Statistics</span>
4388 </h4>
4389 <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px;">
4390 <div>
4391 <div style="font-size: 12px; color: var(--dashboard-text-secondary); margin-bottom: 4px;">Total Deferrals</div>
4392 <div style="font-size: 18px; font-weight: 600; color: var(--dashboard-text);"><?php echo intval($stats['deferrals']); ?></div>
4393 </div>
4394 <div>
4395 <div style="font-size: 12px; color: var(--dashboard-text-secondary); margin-bottom: 4px;">Max Load Observed</div>
4396 <div style="font-size: 18px; font-weight: 600; color: var(--dashboard-text);"><?php echo round($stats['max_load'], 2); ?></div>
4397 </div>
4398 <div>
4399 <div style="font-size: 12px; color: var(--dashboard-text-secondary); margin-bottom: 4px;">Average Load</div>
4400 <div style="font-size: 18px; font-weight: 600; color: var(--dashboard-text);"><?php echo round($stats['avg_load'], 2); ?></div>
4401 </div>
4402 </div>
4403 </div>
4404
4405 <!-- Save Button -->
4406 <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)';">
4407 Save Performance Settings
4408 </button>
4409 </div>
4410 <script>
4411 function updateEffectiveThreshold() {
4412 const coresCount = <?php echo intval($cores); ?>;
4413 const perCoreInput = document.getElementById('cpu_load_per_core_threshold');
4414 const effectiveValue = coresCount * parseFloat(perCoreInput.value);
4415 document.getElementById('effective_threshold_value').textContent = effectiveValue.toFixed(2);
4416 }
4417
4418 function submitPerformanceSettings(event) {
4419 // Prevent any form submission (defensive)
4420 if (event) {
4421 event.preventDefault();
4422 }
4423
4424 // Get the threshold value
4425 const thresholdInput = document.getElementById('cpu_load_per_core_threshold');
4426 if (!thresholdInput) {
4427 console.error('CPU threshold input not found');
4428 return;
4429 }
4430
4431 const threshold = parseFloat(thresholdInput.value);
4432 if (isNaN(threshold) || threshold < 0.5 || threshold > 10.0) {
4433 alert('Please enter a valid threshold between 0.5 and 10.0');
4434 return;
4435 }
4436
4437 // Get AJAX URL
4438 const ajaxUrl = (typeof window.ajaxurl !== 'undefined' && window.ajaxurl)
4439 ? window.ajaxurl
4440 : '<?php echo esc_js(admin_url('admin-ajax.php')); ?>';
4441
4442 // Get nonce from form
4443 const nonceInput = document.querySelector('input[name="meta_sync_nonce"]');
4444 const nonce = nonceInput ? nonceInput.value : '';
4445
4446 // Prepare AJAX request data
4447 const formData = new FormData();
4448 formData.append('action', 'metasync_save_performance_settings');
4449 formData.append('meta_sync_nonce', nonce);
4450 formData.append('metasync_options[performance][cpu_load_per_core_threshold]', threshold);
4451
4452 // Show saving state
4453 const button = event.target;
4454 const originalText = button.innerHTML;
4455 button.innerHTML = '⏳ Saving...';
4456 button.disabled = true;
4457
4458 // Make AJAX request
4459 fetch(ajaxUrl, {
4460 method: 'POST',
4461 body: formData,
4462 headers: {
4463 'X-Requested-With': 'XMLHttpRequest'
4464 }
4465 })
4466 .then(response => response.json())
4467 .then(data => {
4468 button.innerHTML = originalText;
4469 button.disabled = false;
4470
4471 if (data.success) {
4472 // Update the effective threshold display
4473 if (data.data && data.data.effective_threshold) {
4474 document.getElementById('effective_threshold_value').textContent =
4475 data.data.effective_threshold.toFixed(2);
4476 }
4477
4478 // Show success notice
4479 showPerformanceNotice(data.data.message || 'Settings saved successfully!', 'success');
4480 } else {
4481 // Show error notice
4482 showPerformanceNotice(data.data?.message || 'Failed to save settings', 'error');
4483 }
4484 })
4485 .catch(error => {
4486 console.error('AJAX Error:', error);
4487 button.innerHTML = originalText;
4488 button.disabled = false;
4489 showPerformanceNotice('An error occurred while saving settings', 'error');
4490 });
4491 }
4492
4493 function showPerformanceNotice(message, type) {
4494 // Create notice element
4495 const notice = document.createElement('div');
4496 notice.className = `notice notice-${type} is-dismissible`;
4497 notice.style.cssText = 'margin: 20px auto; max-width: 800px;';
4498 notice.innerHTML = `
4499 <p><strong>${type === 'success' ? '�
4500 ' : '❌'} ${message}</strong></p>
4501 <button type="button" class="notice-dismiss" onclick="this.parentElement.remove()" style="cursor: pointer;"></button>
4502 `;
4503
4504 // Find the page header to insert before
4505 const pageHeader = document.querySelector('h1, h2');
4506 if (pageHeader) {
4507 pageHeader.parentElement.insertBefore(notice, pageHeader.nextSibling);
4508 } else {
4509 document.body.insertBefore(notice, document.body.firstChild);
4510 }
4511
4512 // Auto-remove error notices after 5 seconds
4513 if (type === 'error') {
4514 setTimeout(() => {
4515 if (notice.parentElement) {
4516 notice.remove();
4517 }
4518 }, 5000);
4519 }
4520 }
4521 </script>
4522 <?php
4523 }
4524
4525 private function render_reset_settings_section() {
4526 Metasync_Settings_Fields::instance()->render_reset_settings_section();
4527 }
4528
4529 /**
4530 * Render Google Index API section for Indexation Control page
4531 */
4532 public function render_google_index_section() {
4533 Metasync_Settings_Fields::instance()->render_google_index_section();
4534 }
4535
4536 /**
4537 * Render Bing Index (IndexNow) section
4538 *
4539 * @since 2.6.0
4540 * @return void
4541 */
4542 public function render_bing_index_section() {
4543 Metasync_Settings_Fields::instance()->render_bing_index_section();
4544 }
4545
4546 /**
4547 * Render Plugin Access Roles section for Advanced tab accordion
4548 */
4549 private function render_plugin_access_roles_section() {
4550 Metasync_Settings_Fields::instance()->render_plugin_access_roles_section();
4551 }
4552
4553 /**
4554 * Check if the current user has access to the plugin based on role settings
4555 * Wrapper method that delegates to the common Metasync::current_user_has_plugin_access()
4556 * but also requires manage_options capability for admin area access
4557 *
4558 * @return bool True if user has access, false otherwise
4559 */
4560 public function current_user_has_plugin_access() {
4561 return Metasync::current_user_has_plugin_access();
4562 }
4563
4564 /**
4565 * Get default execution settings
4566 *
4567 * @return array Default execution settings
4568 */
4569 private function get_default_execution_settings() {
4570 return Metasync_Settings_Fields::instance()->get_default_execution_settings();
4571 }
4572
4573 /**
4574 * Get execution setting value
4575 *
4576 * @param string $key Setting key
4577 * @param mixed $default Default value if setting doesn't exist
4578 * @return mixed Setting value or default
4579 */
4580 public function get_execution_setting($key, $default = null) {
4581 return Metasync_Settings_Fields::instance()->get_execution_setting($key, $default);
4582 }
4583
4584 /**
4585 * Get all execution settings
4586 *
4587 * @return array All execution settings with defaults merged
4588 */
4589 public function get_all_execution_settings() {
4590 return Metasync_Settings_Fields::instance()->get_all_execution_settings();
4591 }
4592
4593 /**
4594 * Check if server allows changing memory limit
4595 * Tests if ini_set('memory_limit') is allowed
4596 *
4597 * @return bool True if memory limit can be changed, false otherwise
4598 */
4599 private function can_change_memory_limit() {
4600 return Metasync_Settings_Fields::instance()->can_change_memory_limit();
4601 }
4602
4603 /**
4604 * Get PHP server limits for display
4605 *
4606 * @return array Server limits (execution_time, memory_limit, can_change_memory)
4607 */
4608 private function get_server_limits() {
4609 return Metasync_Settings_Fields::instance()->get_server_limits();
4610 }
4611
4612 /**
4613 * Apply memory limit from execution settings
4614 * Only applies if server allows changing memory limit
4615 *
4616 * @return bool True if memory limit was applied, false otherwise
4617 */
4618 public function apply_memory_limit() {
4619 return Metasync_Settings_Fields::instance()->apply_memory_limit();
4620 }
4621
4622 /**
4623 * Parse memory limit string to MB
4624 *
4625 * @param string $memory_limit Memory limit string (e.g., "256M", "1G")
4626 * @return int Memory limit in MB
4627 */
4628 private function parse_memory_limit_to_mb($memory_limit) {
4629 return Metasync_Settings_Fields::instance()->parse_memory_limit_to_mb($memory_limit);
4630 }
4631
4632 /**
4633 * Render Execution Settings section for Advanced tab accordion
4634 */
4635 private function render_execution_settings_section() {
4636 Metasync_Settings_Fields::instance()->render_execution_settings_section();
4637 }
4638
4639 /**
4640 * Get tooltip content for settings fields
4641 *
4642 * @return array Field ID => Tooltip text mapping
4643 */
4644 private function get_field_tooltips() {
4645 return Metasync_Settings_Fields::instance()->get_field_tooltips();
4646 }
4647
4648 /**
4649 * Get the section key for a given field ID
4650 *
4651 * @param string $field_id The settings field ID
4652 * @return string|null Section key or null if not found
4653 */
4654 private function get_field_section($field_id) {
4655 return Metasync_Settings_Fields::instance()->get_field_section($field_id);
4656 }
4657
4658 /**
4659 * Render accordion sections for General Settings
4660 *
4661 * @param string $page The settings page slug
4662 */
4663 public function render_accordion_sections($page) {
4664 Metasync_Settings_Fields::instance()->render_accordion_sections($page);
4665 }
4666
4667 /**
4668 * Register and add settings
4669 */
4670 public function settings_page_init()
4671 {
4672 Metasync_Settings_Registration::instance()->settings_page_init();
4673 }
4674
4675 /**
4676 * Sanitize each setting field as needed
4677 *
4678 * @param array $input Contains all settings fields as array keys
4679 */
4680 public function sanitize($input)
4681 {
4682 return Metasync_Settings_Registration::instance()->sanitize($input);
4683 }
4684
4685 public function metasync_settings_genkey_callback()
4686 {
4687 Metasync_Settings_Fields::instance()->metasync_settings_genkey_callback();
4688 }
4689
4690 /**
4691 * Get the settings option array and print one of its values
4692 */
4693 public function linkgraph_token_callback()
4694 {
4695 Metasync_Settings_Fields::instance()->linkgraph_token_callback();
4696 }
4697
4698
4699 private function time_elapsed_string($datetime, $full = false)
4700 {
4701 return Metasync_Settings_Fields::instance()->time_elapsed_string($datetime, $full);
4702 }
4703
4704 /**
4705 * Get the settings option array and print one of its values
4706 */
4707 public function searchatlas_api_key_callback()
4708 {
4709 Metasync_Settings_Fields::instance()->searchatlas_api_key_callback();
4710 }
4711
4712
4713 /**
4714 * Site Verification Tools
4715 *
4716 * Bing Site Verification
4717 * Baidu Site Verification
4718 * Alexa Site Verification
4719 * Yandex Site Verification
4720 * Google Site Verification
4721 * Pinterest Site Verification
4722 * Norton Safe Web Site Verification
4723 */
4724
4725 /**
4726 * Get the settings option array and print one of its values
4727 */
4728 public function bing_site_verification_callback()
4729 {
4730 Metasync_Settings_Fields::instance()->bing_site_verification_callback();
4731 }
4732
4733
4734
4735
4736
4737 /**
4738 * Get the settings option array and print one of its values
4739 */
4740 public function yandex_site_verification_callback()
4741 {
4742 Metasync_Settings_Fields::instance()->yandex_site_verification_callback();
4743 }
4744
4745 /**
4746 * Get the settings option array and print one of its values
4747 */
4748 public function google_site_verification_callback()
4749 {
4750 Metasync_Settings_Fields::instance()->google_site_verification_callback();
4751 }
4752
4753 /**
4754 * Get the settings option array and print one of its values
4755 */
4756 public function pinterest_site_verification_callback()
4757 {
4758 Metasync_Settings_Fields::instance()->pinterest_site_verification_callback();
4759 }
4760
4761
4762
4763 /**
4764 * Local SEO for business and person
4765 *
4766 */
4767
4768 /**
4769 * Get the settings option array and print one of its values
4770 */
4771 public function local_seo_person_organization_callback()
4772 {
4773 Metasync_Settings_Fields::instance()->local_seo_person_organization_callback();
4774 }
4775
4776 /**
4777 * Get the settings option array and print one of its values
4778 */
4779 public function local_seo_name_callback()
4780 {
4781 Metasync_Settings_Fields::instance()->local_seo_name_callback();
4782 }
4783
4784 /**
4785 * Get the settings option array and print one of its values
4786 */
4787 public function local_seo_logo_callback()
4788 {
4789 Metasync_Settings_Fields::instance()->local_seo_logo_callback();
4790 }
4791
4792 /**
4793 * Get the settings option array and print one of its values
4794 */
4795 public function local_seo_url_callback()
4796 {
4797 Metasync_Settings_Fields::instance()->local_seo_url_callback();
4798 }
4799
4800 /**
4801 * Get the settings option array and print one of its values
4802 */
4803 public function local_seo_email_callback()
4804 {
4805 Metasync_Settings_Fields::instance()->local_seo_email_callback();
4806 }
4807
4808 /**
4809 * Get the settings option array and print one of its values
4810 */
4811 public function local_seo_phone_callback()
4812 {
4813 Metasync_Settings_Fields::instance()->local_seo_phone_callback();
4814 }
4815
4816 /**
4817 * Get the settings option array and print one of its values
4818 */
4819 public function local_seo_address_callback()
4820 {
4821 Metasync_Settings_Fields::instance()->local_seo_address_callback();
4822 }
4823
4824 /**
4825 * Get the settings option array and print one of its values
4826 */
4827 public function local_seo_business_type_callback()
4828 {
4829 Metasync_Settings_Fields::instance()->local_seo_business_type_callback();
4830 }
4831
4832
4833
4834 /**
4835 * Get the settings option array and print one of its values
4836 */
4837 public function local_seo_opening_hours_callback()
4838 {
4839 Metasync_Settings_Fields::instance()->local_seo_opening_hours_callback();
4840 }
4841
4842 /**
4843 * Get the settings option array and print one of its values
4844 */
4845 public function local_seo_phone_numbers_callback()
4846 {
4847 Metasync_Settings_Fields::instance()->local_seo_phone_numbers_callback();
4848 }
4849
4850 /**
4851 * Get the settings option array and print one of its values
4852 */
4853 public function local_seo_price_range_callback()
4854 {
4855 Metasync_Settings_Fields::instance()->local_seo_price_range_callback();
4856 }
4857
4858 /**
4859 * Get the settings option array and print one of its values
4860 */
4861 public function local_seo_about_page_callback()
4862 {
4863 Metasync_Settings_Fields::instance()->local_seo_about_page_callback();
4864 }
4865
4866 /**
4867 * Get the settings option array and print one of its values
4868 */
4869 public function local_seo_contact_page_callback()
4870 {
4871 Metasync_Settings_Fields::instance()->local_seo_contact_page_callback();
4872 }
4873
4874 /**
4875 * Get the settings option array and print one of its values
4876 */
4877 public function local_seo_map_key_callback()
4878 {
4879 Metasync_Settings_Fields::instance()->local_seo_map_key_callback();
4880 }
4881
4882 /**
4883 * Get the settings option array and print one of its values
4884 */
4885 public function local_seo_geo_coordinates_callback()
4886 {
4887 Metasync_Settings_Fields::instance()->local_seo_geo_coordinates_callback();
4888 }
4889
4890 /**
4891 * Get the settings option array and print one of its values
4892 */
4893 public function header_snippets_callback()
4894 {
4895 Metasync_Settings_Fields::instance()->header_snippets_callback();
4896 }
4897
4898 /**
4899 * Get the settings option array and print one of its values
4900 */
4901 public function footer_snippets_callback()
4902 {
4903 Metasync_Settings_Fields::instance()->footer_snippets_callback();
4904 }
4905
4906 /**
4907 * Get the settings option array and print one of its values
4908 */
4909 public function no_index_posts_callback()
4910 {
4911 Metasync_Settings_Fields::instance()->no_index_posts_callback();
4912 }
4913
4914 /**
4915 * Get the settings option array and print one of its values
4916 */
4917 public function no_follow_links_callback()
4918 {
4919 Metasync_Settings_Fields::instance()->no_follow_links_callback();
4920 }
4921
4922 /**
4923 * Get the settings option array and print one of its values
4924 */
4925 public function open_external_links_callback()
4926 {
4927 Metasync_Settings_Fields::instance()->open_external_links_callback();
4928 }
4929
4930 /**
4931 * Get the settings option array and print one of its values
4932 */
4933 public function add_alt_image_tags_callback()
4934 {
4935 Metasync_Settings_Fields::instance()->add_alt_image_tags_callback();
4936 }
4937
4938 /**
4939 * Get the settings option array and print one of its values
4940 */
4941 public function add_title_image_tags_callback()
4942 {
4943 Metasync_Settings_Fields::instance()->add_title_image_tags_callback();
4944 }
4945
4946 /**
4947 * Get the settings option array and print one of its values
4948 */
4949 public function site_type_callback()
4950 {
4951 Metasync_Settings_Fields::instance()->site_type_callback();
4952 }
4953
4954 /**
4955 * Get the settings option array and print one of its values
4956 */
4957 public function site_business_type_callback()
4958 {
4959 Metasync_Settings_Fields::instance()->site_business_type_callback();
4960 }
4961
4962 /**
4963 * Get the settings option array and print one of its values
4964 */
4965 public function site_company_name_callback()
4966 {
4967 Metasync_Settings_Fields::instance()->site_company_name_callback();
4968 }
4969
4970 /**
4971 * Get the settings option array and print one of its values
4972 */
4973 public function site_google_logo_callback()
4974 {
4975 Metasync_Settings_Fields::instance()->site_google_logo_callback();
4976 }
4977
4978 /**
4979 * Get the settings option array and print one of its values
4980 */
4981 public function site_social_share_image_callback()
4982 {
4983 Metasync_Settings_Fields::instance()->site_social_share_image_callback();
4984 }
4985
4986 /**
4987 * Get the settings option array and print one of its values
4988 */
4989 public function common_robot_meta_tags_callback()
4990 {
4991 Metasync_Settings_Fields::instance()->common_robot_meta_tags_callback();
4992 }
4993
4994 /**
4995 * Backward compatibility alias for common_robot_mata_tags_callback
4996 * @deprecated Use common_robot_meta_tags_callback() instead
4997 */
4998 public function common_robot_mata_tags_callback()
4999 {
5000 Metasync_Settings_Fields::instance()->common_robot_mata_tags_callback();
5001 }
5002
5003 /**
5004 * Get the settings option array and print one of its values
5005 */
5006 public function advance_robot_meta_tags_callback()
5007 {
5008 Metasync_Settings_Fields::instance()->advance_robot_meta_tags_callback();
5009 }
5010
5011 /**
5012 * Backward compatibility alias for advance_robot_mata_tags_callback
5013 * @deprecated Use advance_robot_meta_tags_callback() instead
5014 */
5015 public function advance_robot_mata_tags_callback()
5016 {
5017 Metasync_Settings_Fields::instance()->advance_robot_mata_tags_callback();
5018 }
5019
5020 /**
5021 * Get the settings option array and print one of its values
5022 */
5023 public function global_twitter_card_type_callback()
5024 {
5025 Metasync_Settings_Fields::instance()->global_twitter_card_type_callback();
5026 }
5027
5028 /**
5029 * Get the settings option array and print one of its values
5030 */
5031 public function global_open_graph_meta_callback()
5032 {
5033 Metasync_Settings_Fields::instance()->global_open_graph_meta_callback();
5034 }
5035
5036 /**
5037 * Get the settings option array and print one of its values
5038 */
5039 public function global_facebook_meta_callback()
5040 {
5041 Metasync_Settings_Fields::instance()->global_facebook_meta_callback();
5042 }
5043
5044 /**
5045 * Get the settings option array and print one of its values
5046 */
5047 public function global_twitter_meta_callback()
5048 {
5049 Metasync_Settings_Fields::instance()->global_twitter_meta_callback();
5050 }
5051
5052 /**
5053 * Get the settings option array and print one of its values
5054 */
5055 public function og_image_dimensions_callback()
5056 {
5057 Metasync_Settings_Fields::instance()->og_image_dimensions_callback();
5058 }
5059
5060 /**
5061 * Get the settings option array and print one of its values
5062 */
5063 public function article_timestamps_callback()
5064 {
5065 Metasync_Settings_Fields::instance()->article_timestamps_callback();
5066 }
5067
5068 /**
5069 * Get the settings option array and print one of its values
5070 */
5071 public function article_author_callback()
5072 {
5073 Metasync_Settings_Fields::instance()->article_author_callback();
5074 }
5075
5076 /**
5077 * Get the settings option array and print one of its values
5078 */
5079 public function article_section_callback()
5080 {
5081 Metasync_Settings_Fields::instance()->article_section_callback();
5082 }
5083
5084 /**
5085 * Get the settings option array and print one of its values
5086 */
5087 public function article_tags_callback()
5088 {
5089 Metasync_Settings_Fields::instance()->article_tags_callback();
5090 }
5091
5092 /**
5093 * Get the settings option array and print one of its values
5094 */
5095 public function twitter_image_alt_callback()
5096 {
5097 Metasync_Settings_Fields::instance()->twitter_image_alt_callback();
5098 }
5099
5100 /**
5101 * Get the settings option array and print one of its values
5102 */
5103 public function facebook_page_url_callback()
5104 {
5105 Metasync_Settings_Fields::instance()->facebook_page_url_callback();
5106 }
5107
5108 /**
5109 * Get the settings option array and print one of its values
5110 */
5111 public function facebook_authorship_callback()
5112 {
5113 Metasync_Settings_Fields::instance()->facebook_authorship_callback();
5114 }
5115
5116 /**
5117 * Get the settings option array and print one of its values
5118 */
5119 public function facebook_admin_callback()
5120 {
5121 Metasync_Settings_Fields::instance()->facebook_admin_callback();
5122 }
5123
5124 /**
5125 * Get the settings option array and print one of its values
5126 */
5127 public function facebook_app_callback()
5128 {
5129 Metasync_Settings_Fields::instance()->facebook_app_callback();
5130 }
5131
5132 /**
5133 * Get the settings option array and print one of its values
5134 */
5135 public function facebook_secret_callback()
5136 {
5137 Metasync_Settings_Fields::instance()->facebook_secret_callback();
5138 }
5139
5140 /**
5141 * Get the settings option array and print one of its values
5142 */
5143 public function twitter_username_callback()
5144 {
5145 Metasync_Settings_Fields::instance()->twitter_username_callback();
5146 }
5147
5148 /**
5149 * Get business types as choices in local business.
5150 *
5151 * @return array
5152 */
5153 public static function get_business_types()
5154 {
5155 return Metasync_Settings_Fields::get_business_types();
5156 }
5157
5158 /**
5159 * Display a dashboard warning when using the plain permalink structure.
5160 * @param $data An array of data passed.
5161 */
5162 public function permalink_structure_dashboard_warning() {
5163 $current_permalink_structure = get_option('permalink_structure');
5164
5165 # Get the plugin name using centralized method
5166 $plugin_name = Metasync::get_effective_plugin_name();
5167
5168 # Check if the current permalink structure is set to "Plain"
5169 if ($current_permalink_structure === '/%post_id%/' || $current_permalink_structure === '') {
5170 printf(
5171 '<div class="notice notice-error is-dismissible">
5172 <p>
5173 <b>Warning from %s</b><br>
5174 To ensure compatibility, please update your permalink structure to any option other than "Plain".
5175 For any inquiries, contact support.
5176 </p>
5177 </div>',
5178 esc_html($plugin_name)
5179 );
5180 }
5181 }
5182
5183 /**
5184 * Show a one-time admin notice when a page builder is detected but the
5185 * "Default Page Builder" setting has never been explicitly saved.
5186 */
5187 public function display_page_builder_notice() {
5188 $configured = Metasync::get_option('general')['default_page_builder'] ?? '';
5189
5190 // Setting already saved — nothing to warn about
5191 if (!empty($configured)) {
5192 return;
5193 }
5194
5195 // Check if user dismissed this notice
5196 $dismissed = get_user_meta(get_current_user_id(), 'metasync_builder_notice_dismissed', true);
5197 if ($dismissed) {
5198 return;
5199 }
5200
5201 // Handle dismiss action
5202 if (isset($_GET['metasync_dismiss_builder_notice']) && wp_verify_nonce($_GET['_wpnonce'] ?? '', 'metasync_dismiss_builder')) {
5203 update_user_meta(get_current_user_id(), 'metasync_builder_notice_dismissed', '1');
5204 return;
5205 }
5206
5207 require_once plugin_dir_path(dirname(__FILE__)) . 'custom-pages/class-metasync-html-to-builder-converter.php';
5208 $detected = Metasync_HTML_To_Builder_Converter::auto_detect_builder();
5209
5210 // No non-Gutenberg builder detected — no need to warn
5211 if ($detected === 'gutenberg') {
5212 return;
5213 }
5214
5215 $builders = Metasync_HTML_To_Builder_Converter::get_available_builders();
5216 $builder_label = $builders[$detected]['label'] ?? $detected;
5217 $plugin_name = Metasync::get_effective_plugin_name();
5218 $settings_url = admin_url('admin.php?page=' . self::$page_slug . '&tab=general#metasync-section-content_rendering');
5219 $dismiss_url = wp_nonce_url(add_query_arg('metasync_dismiss_builder_notice', '1'), 'metasync_dismiss_builder');
5220
5221 printf(
5222 '<div class="notice notice-info is-dismissible" style="border-left-color: #0073aa;">
5223 <p>
5224 <strong>%s — Page Builder Detected</strong><br>
5225 <strong>%s</strong> is active on this site. Content synced by Content Genius currently uses <strong>Gutenberg (WordPress Block Editor)</strong> format by default.
5226 </p>
5227 <p>
5228 If you want synced content to use %s\'s native widget format instead, you can change this in
5229 <a href="%s"><strong>Settings → Content Rendering → Default Page Builder</strong></a>.
5230 </p>
5231 <p><a href="%s" style="text-decoration: none;">Dismiss this notice</a></p>
5232 </div>',
5233 esc_html($plugin_name),
5234 esc_html($builder_label),
5235 esc_html($builder_label),
5236 esc_url($settings_url),
5237 esc_url($dismiss_url)
5238 );
5239 }
5240
5241 /**
5242 * Display update warning banner if plugin update is available
5243 * Checks WordPress update API to see if a newer version is available
5244 *
5245 * @since 1.0.0
5246 */
5247 public function display_update_warning_banner() {
5248 // Get the installed version from database
5249 $installed_version = get_option('metasync_version', '0.0.0');
5250
5251 // Get plugin basename for WordPress update API check
5252 // This is the plugin file path relative to plugins directory (e.g., 'metasync/metasync.php')
5253 $plugin_file = plugin_basename(plugin_dir_path(dirname(__FILE__)) . 'metasync.php');
5254
5255 // Get WordPress update plugins transient (contains available updates)
5256 $update_plugins = get_site_transient('update_plugins');
5257
5258 // Check if update information exists and if our plugin has an update available
5259 if ($update_plugins && isset($update_plugins->response) && isset($update_plugins->response[$plugin_file])) {
5260 $update_info = $update_plugins->response[$plugin_file];
5261 $latest_version = isset($update_info->new_version) ? $update_info->new_version : '';
5262
5263 // Compare installed version with latest available version
5264 if ($latest_version && version_compare($installed_version, $latest_version, '<')) {
5265 // Get the plugin name using centralized method
5266 $plugin_name = Metasync::get_effective_plugin_name();
5267
5268 // Show admin notice with plugin name included in the message
5269 printf(
5270 '<div class="notice notice-error is-dismissible">
5271 <p>
5272 <b>Warning from %s</b><br>
5273 A new version of %s is available. Please update to the latest version to ensure compatibility and access new features.
5274 For any inquiries, contact support.
5275 </p>
5276 </div>',
5277 esc_html($plugin_name),
5278 esc_html($plugin_name)
5279 );
5280 }
5281 }
5282 }
5283
5284
5285 /*
5286 Method to handle Ajax request from "Indexation Control" page
5287 */
5288 public function meta_sync_save_seo_controls() {
5289 Metasync_Settings_Registration::instance()->meta_sync_save_seo_controls();
5290 }
5291
5292 /**
5293 * AJAX handler for saving Performance (CPU Load) settings
5294 *
5295 * Saves the CPU load threshold and returns statistics
5296 */
5297 public function ajax_save_performance_settings() {
5298 # Check nonce for security and return early if invalid
5299 if (!isset($_POST['meta_sync_nonce']) || !wp_verify_nonce($_POST['meta_sync_nonce'], 'meta_sync_general_setting_nonce')) {
5300 wp_send_json_error(array('message' => 'Invalid nonce'));
5301 return;
5302 }
5303
5304 # Check user capabilities
5305 if (!Metasync::current_user_has_plugin_access()) {
5306 wp_send_json_error(array('message' => 'Insufficient permissions'));
5307 return;
5308 }
5309
5310 # Get current options
5311 $current_options = Metasync::get_option();
5312 if (!is_array($current_options)) {
5313 $current_options = array();
5314 }
5315
5316 # Initialize performance section if it doesn't exist
5317 if (!isset($current_options['performance']) || !is_array($current_options['performance'])) {
5318 $current_options['performance'] = array();
5319 }
5320
5321 # Validate and sanitize CPU load threshold
5322 if (isset($_POST['metasync_options']['performance']['cpu_load_per_core_threshold'])) {
5323 $threshold = floatval($_POST['metasync_options']['performance']['cpu_load_per_core_threshold']);
5324 # Clamp value between 0.5 and 10.0
5325 $threshold = max(0.5, min(10.0, $threshold));
5326 $current_options['performance']['cpu_load_per_core_threshold'] = $threshold;
5327 } else {
5328 # Ensure default value exists
5329 if (!isset($current_options['performance']['cpu_load_per_core_threshold'])) {
5330 $current_options['performance']['cpu_load_per_core_threshold'] = Metasync_CPU_Monitor::DEFAULT_PER_CORE;
5331 }
5332 }
5333
5334 # Save the updated options
5335 $result = Metasync::set_option($current_options);
5336
5337 if ($result) {
5338 # Get current statistics to return
5339 $stats = Metasync_CPU_Monitor::get_stats();
5340 $cores = Metasync_CPU_Monitor::get_cpu_core_count();
5341 $effective_threshold = Metasync_CPU_Monitor::get_effective_threshold();
5342
5343 wp_send_json_success(array(
5344 'message' => 'Performance settings saved successfully!',
5345 'cpu_load_per_core_threshold' => $current_options['performance']['cpu_load_per_core_threshold'],
5346 'effective_threshold' => $effective_threshold,
5347 'cores' => $cores,
5348 'stats' => $stats
5349 ));
5350 } else {
5351 wp_send_json_error(array('message' => 'Failed to save Performance settings'));
5352 }
5353 }
5354
5355 /**
5356 * Schedule transient cleanup cron job
5357 * Runs daily to clean up expired transients and reduce database load
5358 */
5359 public function schedule_transient_cleanup_cron()
5360 {
5361 // Clear any existing scheduled event first
5362 $this->unschedule_transient_cleanup_cron();
5363
5364 // Schedule new cron job daily
5365 if (!wp_next_scheduled('metasync_cleanup_transients')) {
5366 $scheduled = wp_schedule_event(time(), 'metasync_daily_cleanup', 'metasync_cleanup_transients');
5367
5368 if (!$scheduled) {
5369 error_log('MetaSync: Failed to schedule transient cleanup cron job');
5370 }
5371 }
5372 }
5373
5374 /**
5375 * Unschedule transient cleanup cron job
5376 */
5377 public function unschedule_transient_cleanup_cron()
5378 {
5379 $timestamp = wp_next_scheduled('metasync_cleanup_transients');
5380 if ($timestamp) {
5381 wp_unschedule_event($timestamp, 'metasync_cleanup_transients');
5382 error_log('MetaSync: Transient cleanup cron job unscheduled');
5383 }
5384 }
5385
5386 /**
5387 * Maybe schedule transient cleanup cron job
5388 * Called on init hook - always schedules for database maintenance
5389 */
5390 public function maybe_schedule_transient_cleanup_cron()
5391 {
5392 if (!wp_next_scheduled('metasync_cleanup_transients')) {
5393 $this->schedule_transient_cleanup_cron();
5394 }
5395 }
5396
5397 /**
5398 * Schedule hidden post manager cron job (runs every 7 days)
5399 * Called on init hook - always schedules for template checking
5400 */
5401 public function maybe_schedule_hidden_post_check()
5402 {
5403 if (!wp_next_scheduled('metasync_hidden_post_check')) {
5404 $scheduled = wp_schedule_event(time(), 'metasync_weekly', 'metasync_hidden_post_check');
5405
5406 if ($scheduled) {
5407 error_log('MetaSync: Hidden post manager cron job scheduled successfully (runs every 7 days)');
5408 } else {
5409 error_log('MetaSync: Failed to schedule hidden post manager cron job');
5410 }
5411 }
5412 }
5413
5414 /**
5415 * Schedule OTTO 404 exclusion recheck cron job (runs daily)
5416 * Rechecks URLs auto-excluded due to 404 after 7 days; removes from exclusion if now available
5417 */
5418 public function maybe_schedule_otto_recheck_404_cron()
5419 {
5420 if (!wp_next_scheduled('metasync_otto_recheck_404_exclusions')) {
5421 $scheduled = wp_schedule_event(time(), 'metasync_daily_cleanup', 'metasync_otto_recheck_404_exclusions');
5422 if ($scheduled) {
5423 error_log('MetaSync: OTTO 404 recheck cron job scheduled successfully (runs daily)');
5424 } else {
5425 error_log('MetaSync: Failed to schedule OTTO 404 recheck cron job');
5426 }
5427 }
5428 }
5429
5430 /**
5431 * Execute transient cleanup cron job
5432 * Cleans up expired transients and plugin-specific transients to reduce database load
5433 */
5434 public function execute_transient_cleanup()
5435 {
5436 Metasync_Admin_Ajax::instance()->execute_transient_cleanup();
5437 }
5438
5439 // -------------------------------------------------------------------------
5440 // DB CLEANUP — cron scheduling, execution, render, AJAX
5441 // -------------------------------------------------------------------------
5442
5443 /**
5444 * Returns saved DB cleanup settings with defaults merged in.
5445 */
5446 private function get_db_cleanup_settings() {
5447 $defaults = array(
5448 'enabled' => false,
5449 'clean_post_revisions' => true,
5450 'clean_trashed_posts' => true,
5451 'clean_trashed_comments' => true,
5452 'clean_spam_comments' => true,
5453 'clean_expired_transients' => true,
5454 'clean_orphaned_postmeta' => true,
5455 'last_run_at' => 0,
5456 'last_run_stats' => array(),
5457 );
5458 $saved = get_option('metasync_db_cleanup_settings', array());
5459 return array_merge($defaults, $saved);
5460 }
5461
5462 /**
5463 * Schedules the weekly DB cleanup cron if the feature is enabled and not yet scheduled.
5464 */
5465 public function maybe_schedule_db_cleanup_cron() {
5466 $settings = $this->get_db_cleanup_settings();
5467 if (!empty($settings['enabled'])) {
5468 if (!wp_next_scheduled('metasync_db_cleanup')) {
5469 wp_schedule_event(time(), 'metasync_weekly', 'metasync_db_cleanup');
5470 }
5471 } else {
5472 $this->unschedule_db_cleanup_cron();
5473 }
5474 }
5475
5476 /**
5477 * Removes the DB cleanup cron event.
5478 */
5479 public function unschedule_db_cleanup_cron() {
5480 $timestamp = wp_next_scheduled('metasync_db_cleanup');
5481 if ($timestamp) {
5482 wp_unschedule_event($timestamp, 'metasync_db_cleanup');
5483 }
5484 }
5485
5486 /**
5487 * Cron callback: runs each enabled cleanup task and records stats.
5488 * Never calls wp_cache_flush() — only targeted DB deletes.
5489 */
5490 public function execute_db_cleanup() {
5491 global $wpdb;
5492
5493 $settings = $this->get_db_cleanup_settings();
5494 $stats = array();
5495 $start = microtime(true);
5496
5497 try {
5498 // 1. Post revisions
5499 if (!empty($settings['clean_post_revisions'])) {
5500 $stats['post_revisions'] = (int) $wpdb->query(
5501 "DELETE FROM {$wpdb->posts} WHERE post_type = 'revision'"
5502 );
5503 // Remove postmeta left behind by deleted revisions
5504 $wpdb->query(
5505 "DELETE pm FROM {$wpdb->postmeta} pm
5506 LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id
5507 WHERE p.ID IS NULL"
5508 );
5509 }
5510
5511 // 2. Trashed posts + their postmeta
5512 if (!empty($settings['clean_trashed_posts'])) {
5513 // Collect IDs first to cleanly remove postmeta
5514 $trashed_ids = $wpdb->get_col(
5515 "SELECT ID FROM {$wpdb->posts} WHERE post_status = 'trash'"
5516 );
5517 if (!empty($trashed_ids)) {
5518 $placeholders = implode(',', array_fill(0, count($trashed_ids), '%d'));
5519 $wpdb->query(
5520 $wpdb->prepare(
5521 "DELETE FROM {$wpdb->postmeta} WHERE post_id IN ($placeholders)",
5522 $trashed_ids
5523 )
5524 );
5525 $stats['trashed_posts'] = (int) $wpdb->query(
5526 "DELETE FROM {$wpdb->posts} WHERE post_status = 'trash'"
5527 );
5528 } else {
5529 $stats['trashed_posts'] = 0;
5530 }
5531 }
5532
5533 // 3. Trashed comments
5534 if (!empty($settings['clean_trashed_comments'])) {
5535 $stats['trashed_comments'] = (int) $wpdb->query(
5536 "DELETE FROM {$wpdb->comments} WHERE comment_approved = 'trash'"
5537 );
5538 }
5539
5540 // 4. Spam comments
5541 if (!empty($settings['clean_spam_comments'])) {
5542 $stats['spam_comments'] = (int) $wpdb->query(
5543 "DELETE FROM {$wpdb->comments} WHERE comment_approved = 'spam'"
5544 );
5545 }
5546
5547 // 5. Expired transients — direct SQL, no cache flush
5548 if (!empty($settings['clean_expired_transients'])) {
5549 // Delete timeout rows that have already expired
5550 $wpdb->query(
5551 "DELETE FROM {$wpdb->options}
5552 WHERE option_name LIKE '\_transient\_timeout\_%'
5553 AND option_value + 0 < UNIX_TIMESTAMP()"
5554 );
5555 // Delete value rows whose timeout row no longer exists
5556 $stats['expired_transients'] = (int) $wpdb->query(
5557 "DELETE o FROM {$wpdb->options} o
5558 LEFT JOIN {$wpdb->options} t
5559 ON t.option_name = CONCAT('_transient_timeout_', SUBSTRING(o.option_name, 12))
5560 WHERE o.option_name LIKE '\_transient\_%'
5561 AND o.option_name NOT LIKE '\_transient\_timeout\_%'
5562 AND t.option_id IS NULL"
5563 );
5564 }
5565
5566 // 6. Orphaned postmeta (post_id references a post that no longer exists)
5567 if (!empty($settings['clean_orphaned_postmeta'])) {
5568 $stats['orphaned_postmeta'] = (int) $wpdb->query(
5569 "DELETE pm FROM {$wpdb->postmeta} pm
5570 LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id
5571 WHERE p.ID IS NULL"
5572 );
5573 }
5574
5575 $stats['execution_ms'] = round((microtime(true) - $start) * 1000, 2);
5576
5577 // Persist last-run timestamp and stats
5578 $settings['last_run_at'] = time();
5579 $settings['last_run_stats'] = $stats;
5580 update_option('metasync_db_cleanup_settings', $settings);
5581
5582 error_log('MetaSync: DB cleanup completed — ' . json_encode($stats));
5583
5584 } catch (Exception $e) {
5585 error_log('MetaSync: DB cleanup failed — ' . $e->getMessage());
5586 }
5587 }
5588
5589 /**
5590 * Renders the Database Cleanup accordion section in Advanced Settings.
5591 */
5592 public function render_db_cleanup_section() {
5593 $settings = $this->get_db_cleanup_settings();
5594 $last_run = !empty($settings['last_run_at']) ? $settings['last_run_at'] : 0;
5595 $stats = !empty($settings['last_run_stats']) ? $settings['last_run_stats'] : array();
5596 $next_run = wp_next_scheduled('metasync_db_cleanup');
5597
5598 $task_labels = array(
5599 'clean_post_revisions' => 'Post revisions',
5600 'clean_trashed_posts' => 'Trashed posts',
5601 'clean_trashed_comments' => 'Trashed comments',
5602 'clean_spam_comments' => 'Spam comments',
5603 'clean_expired_transients' => 'Expired transients',
5604 'clean_orphaned_postmeta' => 'Orphaned post meta',
5605 );
5606 ?>
5607 <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px;">
5608 <p style="color: var(--dashboard-text-secondary); margin: 0 0 20px 0;">
5609 Remove orphaned database rows that accumulate over time and slow down queries. Runs weekly via WP-Cron when enabled.
5610 </p>
5611
5612 <form id="metasync-db-cleanup-settings-form" method="post">
5613 <?php wp_nonce_field('metasync_db_cleanup_settings_nonce', 'db_cleanup_settings_nonce'); ?>
5614
5615 <!-- Enable weekly cleanup -->
5616 <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;">
5617 <label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
5618 <input type="checkbox"
5619 id="db_cleanup_enabled"
5620 name="enabled"
5621 value="1"
5622 <?php checked(!empty($settings['enabled'])); ?>
5623 style="width: 16px; height: 16px; cursor: pointer;" />
5624 <span style="color: var(--dashboard-text-primary); font-weight: 600; font-size: 14px;">
5625 Enable Weekly AI Cleanup
5626 </span>
5627 </label>
5628 <p style="color: var(--dashboard-text-secondary); font-size: 12px; margin: 8px 0 0 26px;">
5629 Schedules an automatic cleanup every 7 days via WP-Cron.
5630 <?php if ($next_run): ?>
5631 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>
5632 <?php endif; ?>
5633 </p>
5634 </div>
5635
5636 <!-- Cleanup tasks -->
5637 <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;">
5638 <h3 style="color: var(--dashboard-text-primary); margin: 0 0 16px 0; font-size: 16px; font-weight: 600;">Cleanup Tasks</h3>
5639 <?php foreach ($task_labels as $key => $label): ?>
5640 <label style="display: flex; align-items: center; gap: 10px; margin-bottom: 12px; cursor: pointer;">
5641 <input type="checkbox"
5642 name="<?php echo esc_attr($key); ?>"
5643 value="1"
5644 <?php checked(!empty($settings[$key])); ?>
5645 style="width: 16px; height: 16px; cursor: pointer;" />
5646 <span style="color: var(--dashboard-text-primary); font-size: 14px;"><?php echo esc_html($label); ?></span>
5647 </label>
5648 <?php endforeach; ?>
5649 </div>
5650
5651 <!-- Last run info -->
5652 <div id="metasync-db-cleanup-last-run"
5653 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;'; ?>">
5654 <h3 style="color: var(--dashboard-text-primary); margin: 0 0 12px 0; font-size: 16px; font-weight: 600;">Last Cleanup</h3>
5655 <p id="metasync-db-cleanup-last-run-time" style="color: var(--dashboard-text-secondary); font-size: 13px; margin: 0 0 10px 0;">
5656 <?php echo $last_run ? esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $last_run)) : ''; ?>
5657 </p>
5658 <div id="metasync-db-cleanup-stats" style="display: flex; flex-wrap: wrap; gap: 10px;">
5659 <?php foreach ($stats as $stat_key => $count): ?>
5660 <?php if ($stat_key === 'execution_ms') continue; ?>
5661 <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;">
5662 <?php echo esc_html(str_replace('_', ' ', $stat_key)); ?>: <?php echo intval($count); ?>
5663 </span>
5664 <?php endforeach; ?>
5665 <?php if (!empty($stats['execution_ms'])): ?>
5666 <span style="color: var(--dashboard-text-secondary); font-size: 12px; align-self: center;">
5667 in <?php echo esc_html($stats['execution_ms']); ?>ms
5668 </span>
5669 <?php endif; ?>
5670 </div>
5671 </div>
5672
5673 <!-- Buttons -->
5674 <div style="display: flex; gap: 12px; align-items: center; margin-top: 4px;">
5675 <button type="submit"
5676 id="metasync-db-cleanup-save-btn"
5677 class="button button-primary"
5678 style="padding: 10px 20px; font-size: 14px; font-weight: 500;">
5679 <span class="save-text">Save Settings</span>
5680 <span class="save-spinner" style="display:none; margin-left: 8px;"></span>
5681 </button>
5682 <button type="button"
5683 id="metasync-db-cleanup-run-btn"
5684 class="button"
5685 style="padding: 10px 20px; font-size: 14px; font-weight: 500;">
5686 <span class="run-text">Run Cleanup Now</span>
5687 <span class="run-spinner" style="display:none; margin-left: 8px;"></span>
5688 </button>
5689 </div>
5690
5691 <!-- Messages -->
5692 <div id="metasync-db-cleanup-message" style="display:none; margin-top: 16px; padding: 12px; border-radius: 6px;"></div>
5693 </form>
5694 </div>
5695
5696 <script>
5697 jQuery(document).ready(function($) {
5698 var $form = $('#metasync-db-cleanup-settings-form');
5699 var $saveBtn = $('#metasync-db-cleanup-save-btn');
5700 var $runBtn = $('#metasync-db-cleanup-run-btn');
5701 var $message = $('#metasync-db-cleanup-message');
5702
5703 function showMessage(text, type) {
5704 $message.css({
5705 'background' : type === 'success' ? 'rgba(34,197,94,0.1)' : 'rgba(239,68,68,0.1)',
5706 'border' : '1px solid ' + (type === 'success' ? 'rgba(34,197,94,0.3)' : 'rgba(239,68,68,0.3)'),
5707 'color' : type === 'success' ? '#22c55e' : '#ef4444',
5708 'padding' : '12px 16px',
5709 'border-radius' : '6px',
5710 'font-size' : '14px',
5711 'line-height': '1.5',
5712 'display' : 'block'
5713 }).html('<strong style="margin-right:8px;">' + (type === 'success' ? '' : '') + '</strong>' + text).show();
5714
5715 if (type === 'success') {
5716 setTimeout(function() { $message.fadeOut(300); }, 5000);
5717 }
5718 }
5719
5720 // Save settings
5721 $form.on('submit', function(e) {
5722 e.preventDefault();
5723 $saveBtn.prop('disabled', true);
5724 $saveBtn.find('.save-text').text('Saving...');
5725 $saveBtn.find('.save-spinner').show();
5726 $message.hide();
5727
5728 $.ajax({
5729 url : ajaxurl,
5730 type : 'POST',
5731 data : $form.serialize() + '&action=metasync_save_db_cleanup_settings',
5732 success: function(response) {
5733 $saveBtn.prop('disabled', false);
5734 $saveBtn.find('.save-text').text('Save Settings');
5735 $saveBtn.find('.save-spinner').hide();
5736 if (response.success) {
5737 showMessage(response.data.message, 'success');
5738 // Update next-run label if returned
5739 if (response.data.next_run_label) {
5740 $('#db_cleanup_enabled').closest('label')
5741 .next('p').find('strong').text(response.data.next_run_label);
5742 }
5743 } else {
5744 showMessage(response.data.message || 'Error saving settings.', 'error');
5745 }
5746 },
5747 error: function() {
5748 $saveBtn.prop('disabled', false);
5749 $saveBtn.find('.save-text').text('Save Settings');
5750 $saveBtn.find('.save-spinner').hide();
5751 showMessage('An error occurred. Please try again.', 'error');
5752 }
5753 });
5754 });
5755
5756 // Run cleanup now — sends current form state so unsaved changes are respected
5757 $runBtn.on('click', function() {
5758 $runBtn.prop('disabled', true);
5759 $runBtn.find('.run-text').text('Running...');
5760 $runBtn.find('.run-spinner').show();
5761 $message.hide();
5762
5763 $.ajax({
5764 url : ajaxurl,
5765 type : 'POST',
5766 data : $form.serialize() + '&action=metasync_run_db_cleanup',
5767 success: function(response) {
5768 $runBtn.prop('disabled', false);
5769 $runBtn.find('.run-text').text('Run Cleanup Now');
5770 $runBtn.find('.run-spinner').hide();
5771
5772 if (response.success) {
5773 showMessage(response.data.message, 'success');
5774
5775 // Update last-run panel
5776 if (response.data.timestamp_label) {
5777 $('#metasync-db-cleanup-last-run').show();
5778 $('#metasync-db-cleanup-last-run-time').text(response.data.timestamp_label);
5779 }
5780 if (response.data.stats_html) {
5781 $('#metasync-db-cleanup-stats').html(response.data.stats_html);
5782 }
5783 } else {
5784 showMessage(response.data.message || 'Cleanup failed.', 'error');
5785 }
5786 },
5787 error: function() {
5788 $runBtn.prop('disabled', false);
5789 $runBtn.find('.run-text').text('Run Cleanup Now');
5790 $runBtn.find('.run-spinner').hide();
5791 showMessage('An error occurred. Please try again.', 'error');
5792 }
5793 });
5794 });
5795 });
5796 </script>
5797 <?php
5798 }
5799
5800 /**
5801 * AJAX: Save DB cleanup settings and reschedule cron accordingly.
5802 */
5803 public function ajax_save_db_cleanup_settings() {
5804 if (!isset($_POST['db_cleanup_settings_nonce']) ||
5805 !wp_verify_nonce($_POST['db_cleanup_settings_nonce'], 'metasync_db_cleanup_settings_nonce')) {
5806 wp_send_json_error(array('message' => 'Invalid security token. Please refresh the page and try again.'));
5807 return;
5808 }
5809
5810 if (!Metasync::current_user_has_plugin_access()) {
5811 wp_send_json_error(array('message' => 'Insufficient permissions.'));
5812 return;
5813 }
5814
5815 $existing = $this->get_db_cleanup_settings();
5816
5817 $task_keys = array(
5818 'clean_post_revisions',
5819 'clean_trashed_posts',
5820 'clean_trashed_comments',
5821 'clean_spam_comments',
5822 'clean_expired_transients',
5823 'clean_orphaned_postmeta',
5824 );
5825
5826 $new_settings = array(
5827 'enabled' => !empty($_POST['enabled']),
5828 'last_run_at' => $existing['last_run_at'],
5829 'last_run_stats' => $existing['last_run_stats'],
5830 );
5831
5832 foreach ($task_keys as $key) {
5833 $new_settings[$key] = !empty($_POST[$key]);
5834 }
5835
5836 update_option('metasync_db_cleanup_settings', $new_settings);
5837
5838 // Reschedule based on new enabled state
5839 if (!empty($new_settings['enabled'])) {
5840 if (!wp_next_scheduled('metasync_db_cleanup')) {
5841 wp_schedule_event(time(), 'metasync_weekly', 'metasync_db_cleanup');
5842 }
5843 $next_run = wp_next_scheduled('metasync_db_cleanup');
5844 $next_run_label = $next_run
5845 ? date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $next_run)
5846 : '';
5847 wp_send_json_success(array(
5848 'message' => 'Settings saved. Weekly cleanup is enabled.',
5849 'next_run_label' => $next_run_label,
5850 ));
5851 } else {
5852 $this->unschedule_db_cleanup_cron();
5853 wp_send_json_success(array(
5854 'message' => 'Settings saved. Weekly cleanup is disabled.',
5855 ));
5856 }
5857 }
5858
5859 /**
5860 * AJAX: Manually trigger the DB cleanup and return stats for the UI.
5861 * Persists current form state first so unsaved checkbox changes are respected.
5862 */
5863 public function ajax_run_db_cleanup() {
5864 if (!isset($_POST['db_cleanup_settings_nonce']) ||
5865 !wp_verify_nonce($_POST['db_cleanup_settings_nonce'], 'metasync_db_cleanup_settings_nonce')) {
5866 wp_send_json_error(array('message' => 'Invalid security token.'));
5867 return;
5868 }
5869
5870 if (!Metasync::current_user_has_plugin_access()) {
5871 wp_send_json_error(array('message' => 'Insufficient permissions.'));
5872 return;
5873 }
5874
5875 // Save current form state before running so the cleanup uses what the user sees
5876 $existing = $this->get_db_cleanup_settings();
5877 $task_keys = array(
5878 'clean_post_revisions',
5879 'clean_trashed_posts',
5880 'clean_trashed_comments',
5881 'clean_spam_comments',
5882 'clean_expired_transients',
5883 'clean_orphaned_postmeta',
5884 );
5885 $to_save = array(
5886 'enabled' => !empty($_POST['enabled']),
5887 'last_run_at' => $existing['last_run_at'],
5888 'last_run_stats' => $existing['last_run_stats'],
5889 );
5890 foreach ($task_keys as $key) {
5891 $to_save[$key] = !empty($_POST[$key]);
5892 }
5893 update_option('metasync_db_cleanup_settings', $to_save);
5894
5895 // Reschedule cron to match the (possibly updated) enabled state
5896 if (!empty($to_save['enabled'])) {
5897 if (!wp_next_scheduled('metasync_db_cleanup')) {
5898 wp_schedule_event(time(), 'metasync_weekly', 'metasync_db_cleanup');
5899 }
5900 } else {
5901 $this->unschedule_db_cleanup_cron();
5902 }
5903
5904 $this->execute_db_cleanup();
5905
5906 $settings = $this->get_db_cleanup_settings();
5907 $stats = $settings['last_run_stats'];
5908 $timestamp_label = date_i18n(
5909 get_option('date_format') . ' ' . get_option('time_format'),
5910 $settings['last_run_at']
5911 );
5912
5913 // Build stats badges HTML
5914 $stat_labels = array(
5915 'post_revisions' => 'Post revisions',
5916 'trashed_posts' => 'Trashed posts',
5917 'trashed_comments' => 'Trashed comments',
5918 'spam_comments' => 'Spam comments',
5919 'expired_transients' => 'Expired transients',
5920 'orphaned_postmeta' => 'Orphaned post meta',
5921 );
5922
5923 $stats_html = '';
5924 foreach ($stat_labels as $key => $label) {
5925 if (isset($stats[$key])) {
5926 $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;">'
5927 . esc_html($label) . ': ' . intval($stats[$key]) . '</span> ';
5928 }
5929 }
5930 if (!empty($stats['execution_ms'])) {
5931 $stats_html .= '<span style="color:var(--dashboard-text-secondary);font-size:12px;align-self:center;">in '
5932 . esc_html($stats['execution_ms']) . 'ms</span>';
5933 }
5934
5935 wp_send_json_success(array(
5936 'message' => 'Cleanup completed successfully.',
5937 'timestamp_label' => $timestamp_label,
5938 'stats_html' => $stats_html,
5939 ));
5940 }
5941
5942 /**
5943 * Control plugin auto-updates based on user setting
5944 *
5945 * @param bool $update Whether to update
5946 * @param object $item Update offer
5947 * @return bool Whether to allow auto-update
5948 */
5949 public function control_plugin_auto_updates($update, $item)
5950 {
5951 # Check if the item object has the slug property
5952 if (!isset($item->slug)) {
5953 return $update;
5954 }
5955
5956 // Check if this is our plugin
5957 if ($item->slug === 'metasync') {
5958 $general_settings = Metasync::get_option('general') ?? [];
5959 $enable_auto_updates = $general_settings['enable_auto_updates'] ?? false;
5960
5961 // Return the user's preference (true = allow auto-updates, false = prevent)
5962 return $enable_auto_updates === 'true' || $enable_auto_updates === true;
5963 }
5964
5965 // For other plugins, don't interfere with their auto-update settings
5966 return $update;
5967 }
5968
5969 /**
5970 * AJAX handler to add excluded URL for OTTO
5971 */
5972 public function ajax_otto_add_excluded_url()
5973 {
5974 Metasync_Otto_Cache_Manager::instance()->ajax_otto_add_excluded_url();
5975 }
5976
5977 /**
5978 * AJAX handler to delete excluded URL for OTTO
5979 */
5980 public function ajax_otto_delete_excluded_url()
5981 {
5982 Metasync_Otto_Cache_Manager::instance()->ajax_otto_delete_excluded_url();
5983 }
5984
5985 /**
5986 * AJAX handler to recheck if an excluded URL is now available
5987 * Used for "Recheck" action on Excluded URLs list
5988 */
5989 public function ajax_otto_recheck_excluded_url()
5990 {
5991 Metasync_Otto_Cache_Manager::instance()->ajax_otto_recheck_excluded_url();
5992 }
5993
5994 /**
5995 * AJAX handler to get excluded URLs with pagination
5996 */
5997 public function ajax_otto_get_excluded_urls()
5998 {
5999 Metasync_Otto_Cache_Manager::instance()->ajax_otto_get_excluded_urls();
6000 }
6001
6002 /**
6003 * AJAX handler for submitting issue reports to Sentry
6004 *
6005 * @since 2.5.10
6006 * @return void Sends JSON response and exits
6007 */
6008 public function ajax_submit_issue_report()
6009 {
6010 Metasync_Admin_Ajax::instance()->ajax_submit_issue_report();
6011 }
6012
6013 /**
6014 * Format duration seconds into human-readable label
6015 *
6016 * @since 2.5.11
6017 * @param int $seconds Duration in seconds
6018 * @return string Human-readable duration
6019 */
6020 private function format_duration_label($seconds)
6021 {
6022 $labels = array(
6023 3600 => '1 hour',
6024 14400 => '4 hours',
6025 28800 => '8 hours',
6026 86400 => '24 hours',
6027 172800 => '48 hours',
6028 604800 => '7 days',
6029 1209600 => '14 days',
6030 2592000 => '30 days'
6031 );
6032
6033 if (isset($labels[$seconds])) {
6034 return $labels[$seconds];
6035 }
6036
6037 # Calculate hours if not a standard duration
6038 $hours = round($seconds / 3600);
6039 return $hours . ' hours';
6040 }
6041
6042 /**
6043 * AJAX handler for password recovery
6044 * Sends the whitelabel settings password to the configured recovery email
6045 */
6046 public function ajax_recover_password()
6047 {
6048 Metasync_Admin_Ajax::instance()->ajax_recover_password();
6049 }
6050
6051 /**
6052 * AJAX handler for saving theme preference
6053 * Saves the user's theme choice (light/dark) to WordPress options
6054 */
6055 public function ajax_save_theme()
6056 {
6057 Metasync_Admin_Ajax::instance()->ajax_save_theme();
6058 }
6059
6060 /**
6061 * AJAX handler for tracking 1-click activation in GA4
6062 */
6063 public function ajax_track_one_click_activation()
6064 {
6065 Metasync_Admin_Ajax::instance()->ajax_track_one_click_activation();
6066 }
6067
6068 /**
6069 * Handler for exporting whitelabel settings to a zip file
6070 * Uses admin-post action for file downloads
6071 */
6072 public function handle_export_whitelabel_settings()
6073 {
6074 Metasync_Admin_Ajax::instance()->handle_export_whitelabel_settings();
6075 }
6076
6077
6078 /**
6079 * Add custom column to posts/pages list for HTML-converted pages
6080 *
6081 * @param array $columns Existing columns
6082 * @return array Modified columns
6083 */
6084 public function add_html_converted_column($columns)
6085 {
6086 // Add column after the title column
6087 $new_columns = array();
6088 foreach ($columns as $key => $value) {
6089 $new_columns[$key] = $value;
6090 if ($key === 'title') {
6091 $new_columns['metasync_html_source'] = __('Source', 'metasync');
6092 }
6093 }
6094 return $new_columns;
6095 }
6096
6097 /**
6098 * Render content for the HTML-converted column
6099 *
6100 * @param string $column_name Name of the column
6101 * @param int $post_id Post ID
6102 */
6103 public function render_html_converted_column($column_name, $post_id)
6104 {
6105 if ($column_name !== 'metasync_html_source') {
6106 return;
6107 }
6108
6109 // Check if this is an HTML-converted page
6110 $has_raw_html = get_post_meta($post_id, '_metasync_raw_html_enabled', true);
6111 $has_custom_css = get_post_meta($post_id, '_metasync_custom_css', true);
6112
6113 // If page has raw HTML or custom CSS from conversion, show badge
6114 if ($has_raw_html || !empty($has_custom_css)) {
6115 $label = $this->get_html_source_label();
6116 $tooltip = sprintf(
6117 __('This page was created using %s HTML-to-Builder converter', 'metasync'),
6118 $label
6119 );
6120
6121 echo sprintf(
6122 '<span class="metasync-html-badge" title="%s">
6123 <span class="metasync-badge-icon">⚡</span>
6124 <span class="metasync-badge-text">%s</span>
6125 </span>',
6126 esc_attr($tooltip),
6127 esc_html($label)
6128 );
6129 }
6130 }
6131
6132 /**
6133 * Get the label for HTML-converted pages (respects whitelabel settings)
6134 *
6135 * @return string Label to display
6136 */
6137 private function get_html_source_label()
6138 {
6139 $whitelabel_company = Metasync::get_whitelabel_company_name();
6140 if (!empty($whitelabel_company)) {
6141 return $whitelabel_company . ' AI';
6142 }
6143 return Metasync::get_effective_plugin_name() . ' AI';
6144 }
6145
6146 /**
6147 * Add source notice banner in the page editor
6148 *
6149 * @param WP_Post $post Current post object
6150 */
6151 public function add_editor_source_notice($post)
6152 {
6153 if (!$post || !in_array($post->post_type, array('post', 'page'))) {
6154 return;
6155 }
6156
6157 // Don't show the "HTML-to-Builder converter" banner on LPS-synced / custom-HTML
6158 // pages: those are raw-HTML store-and-serve pages, NOT actually converted to a
6159 // page builder, so the banner mislabels them. It still shows for pages genuinely
6160 // produced by the converter.
6161 if (function_exists('metasync_is_custom_or_lps_page') && metasync_is_custom_or_lps_page($post->ID)) {
6162 return;
6163 }
6164
6165 $has_raw_html = get_post_meta($post->ID, '_metasync_raw_html_enabled', true);
6166 $has_custom_css = get_post_meta($post->ID, '_metasync_custom_css', true);
6167
6168 if ($has_raw_html || !empty($has_custom_css)) {
6169 $label = $this->get_html_source_label();
6170 $message = sprintf(
6171 __('This page was created using %s HTML-to-Builder converter. The design is preserved with custom CSS and inline styles.', 'metasync'),
6172 '<strong>' . esc_html($label) . '</strong>'
6173 );
6174
6175 echo sprintf(
6176 '<div class="metasync-editor-notice notice notice-info is-dismissible">
6177 <div class="metasync-editor-notice-content">
6178 <span class="metasync-editor-badge">
6179 <span class="metasync-badge-icon">⚡</span>
6180 <span class="metasync-badge-text">%s</span>
6181 </span>
6182 <p class="metasync-editor-message">%s</p>
6183 </div>
6184 </div>',
6185 esc_html($label),
6186 $message
6187 );
6188 }
6189 }
6190
6191 /**
6192 * Add source display in quick edit panel
6193 *
6194 * @param string $column_name Column name
6195 * @param string $post_type Post type
6196 */
6197 public function add_quick_edit_source_display($column_name, $post_type)
6198 {
6199 if ($column_name !== 'metasync_html_source') {
6200 return;
6201 }
6202
6203 if (!in_array($post_type, array('post', 'page'))) {
6204 return;
6205 }
6206
6207 ?>
6208 <fieldset class="inline-edit-col-left metasync-quick-edit-source">
6209 <div class="inline-edit-col">
6210 <label>
6211 <span class="title"><?php _e('Source', 'metasync'); ?></span>
6212 <span class="metasync-quick-edit-badge-container"></span>
6213 </label>
6214 </div>
6215 </fieldset>
6216 <?php
6217 }
6218
6219 /**
6220 * Add dashboard widget for HTML-converted pages
6221 */
6222 public function add_html_pages_dashboard_widget()
6223 {
6224 $label = $this->get_html_source_label();
6225 $widget_title = sprintf(__('%s Pages', 'metasync'), $label);
6226
6227 wp_add_dashboard_widget(
6228 'metasync_html_pages_widget',
6229 $widget_title,
6230 array($this, 'render_html_pages_dashboard_widget')
6231 );
6232 }
6233
6234 /**
6235 * Render the dashboard widget content
6236 */
6237 public function render_html_pages_dashboard_widget()
6238 {
6239 Metasync_Admin_Ajax::instance()->render_html_pages_dashboard_widget();
6240 }
6241
6242 /**
6243 * Bot Statistics page callback
6244 * Displays bot detection statistics and logs
6245 */
6246 public function create_admin_bot_statistics_page()
6247 {
6248 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-otto-bot-statistics.php';
6249 }
6250
6251 /**
6252 * AJAX handler for resetting bot statistics
6253 */
6254 public function ajax_reset_bot_stats()
6255 {
6256 check_ajax_referer('metasync_reset_bot_stats', 'nonce');
6257
6258 if (!Metasync::current_user_has_plugin_access()) {
6259 wp_send_json_error(['message' => 'Insufficient permissions.']);
6260 }
6261
6262 require_once plugin_dir_path(dirname(__FILE__)) . 'otto/class-metasync-otto-bot-statistics-database.php';
6263 $db = Metasync_Otto_Bot_Statistics_Database::get_instance();
6264
6265 $result = $db->reset_statistics();
6266
6267 if ($result) {
6268 wp_send_json_success(['message' => 'Statistics reset successfully.']);
6269 } else {
6270 wp_send_json_error(['message' => 'Failed to reset statistics.']);
6271 }
6272 }
6273
6274 /**
6275 * AJAX handler for sending URLs to Google Instant Indexing API
6276 *
6277 * @since 2.6.0
6278 * @return void Sends JSON response and exits
6279 */
6280 public function ajax_send_giapi()
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 $post_data = metasync_sanitize_input_array($_POST);
6289 if (!isset($post_data['metasync_giapi_url'])) {
6290 return;
6291 }
6292
6293 // Parse URLs from textarea input (one per line)
6294 $urls = array_values(array_filter(array_map('trim', explode("\n", sanitize_textarea_field(wp_unslash($post_data['metasync_giapi_url']))))));
6295
6296 if (empty($urls)) {
6297 return;
6298 }
6299
6300 if (!isset($post_data['metasync_giapi_action'])) {
6301 return;
6302 }
6303 $action = sanitize_title($post_data['metasync_giapi_action']);
6304
6305 // Map form action values to google_index_direct action values
6306 if ($action === 'remove') {
6307 $action = 'delete';
6308 }
6309
6310 header('Content-type: application/json');
6311
6312 $result_data = [];
6313 foreach ($urls as $i => $url) {
6314 $url = esc_url_raw($url);
6315 if (empty($url)) {
6316 continue;
6317 }
6318
6319 if ($action === 'status') {
6320 $result = google_index_direct()->get_url_status($url);
6321 } else {
6322 $result = google_index_direct()->index_url($url, $action);
6323 }
6324
6325 $key = 'url-' . $i;
6326 if (!empty($result['success'])) {
6327 $result_data[$key] = $result['data'];
6328 } else {
6329 $result_data[$key] = (object) [
6330 'error' => (object) [
6331 'code' => isset($result['error']['code']) ? $result['error']['code'] : 400,
6332 'message' => isset($result['error']['message']) ? $result['error']['message'] : 'Unknown error',
6333 ]
6334 ];
6335 }
6336 }
6337
6338 // For single URL, unwrap from the batch format (matches old behavior)
6339 if (count($result_data) === 1) {
6340 $result_data = reset($result_data);
6341 }
6342
6343 wp_send_json($result_data);
6344 wp_die();
6345 }
6346
6347 /**
6348 * AJAX handler for sending URLs to Bing via IndexNow API
6349 *
6350 * @since 2.6.0
6351 * @return void Sends JSON response and exits
6352 */
6353 public function ajax_send_bing_indexnow()
6354 {
6355 check_ajax_referer('metasync_nonce', 'nonce');
6356
6357 if (!Metasync::current_user_has_plugin_access()) {
6358 wp_send_json_error(['message' => 'Insufficient permissions.'], 403);
6359 }
6360
6361 require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php';
6362 $bing_instant_index = new Metasync_Bing_Instant_Index();
6363 $bing_instant_index->send();
6364 }
6365
6366 /**
6367 * Save instant indexing settings (Google and Bing)
6368 *
6369 * @since 2.6.0
6370 * @return void
6371 */
6372 public function save_instant_indexing_settings()
6373 {
6374 // Check if this is a settings submission
6375 if (!isset($_POST['submit'])) {
6376 return;
6377 }
6378
6379 // Save post types for Google Instant Indexing auto-submit
6380 if (isset($_POST['metasync_post_types'])) {
6381 $post_data = metasync_sanitize_input_array($_POST);
6382 $post_types = is_array($post_data['metasync_post_types']) ? array_map('sanitize_title', $post_data['metasync_post_types']) : [];
6383
6384 $settings = get_option('metasync_options_instant_indexing', ['post_types' => []]);
6385 $settings['post_types'] = array_values($post_types);
6386 update_option('metasync_options_instant_indexing', $settings);
6387 }
6388
6389 // Note: Bing Instant Indexing settings are saved via AJAX in save_bing_inline_settings_ajax()
6390 }
6391
6392 /**
6393 * Save Bing instant indexing settings from inline form (Indexation Control page)
6394 *
6395 * @since 2.6.0
6396 * @return bool True on success, false on failure
6397 */
6398 private function save_bing_inline_settings_ajax() {
6399 return Metasync_Settings_Registration::instance()->save_bing_inline_settings_ajax();
6400 }
6401
6402 /**
6403 * Add instant indexing action links to post/page rows
6404 *
6405 * @since 2.6.0
6406 * @param array $actions Current actions
6407 * @param WP_Post $post Current post object
6408 * @return array Modified actions
6409 */
6410 public function add_instant_indexing_post_actions($actions, $post)
6411 {
6412 // Add Google Instant Indexing links
6413 $options = get_option('metasync_options_instant_indexing', ['json_key' => '', 'post_types' => []]);
6414 $post_types = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : [];
6415
6416 if (in_array($post->post_type, $post_types) && $post->post_status == 'publish') {
6417 $link = get_permalink($post);
6418
6419 // Get menu slug (support white label)
6420 $general_options = Metasync::get_option('general') ?? [];
6421 $menu_slug = !empty($general_options['white_label_plugin_menu_slug']) ? $general_options['white_label_plugin_menu_slug'] : 'searchatlas';
6422 $page_slug = $menu_slug . '-google-console';
6423
6424 $actions['index-update'] = '<a href="' . admin_url("admin.php?page=" . $page_slug . "&postaction=update&posturl=" . rawurlencode($link)) . '" title="" rel="permalink">Update Google Index</a>';
6425 $actions['index-status'] = '<a href="' . admin_url("admin.php?page=" . $page_slug . "&postaction=status&posturl=" . rawurlencode($link)) . '" title="" rel="permalink">Status Google Index</a>';
6426 }
6427
6428 // Add Bing Instant Indexing links
6429 require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php';
6430 $bing_instant_index = new Metasync_Bing_Instant_Index();
6431 $actions = $bing_instant_index->bing_instant_index_post_link($actions, $post);
6432
6433 return $actions;
6434 }
6435
6436 /**
6437 * Auto-submit post to instant indexing services when published
6438 *
6439 * @since 2.6.0
6440 * @param int $post_id Post ID
6441 * @param WP_Post $post Post object
6442 * @param bool $update Whether this is an update
6443 * @return void
6444 */
6445 public function auto_submit_to_instant_indexing($post_id, $post, $update)
6446 {
6447 // Skip revisions, autosaves, and non-published posts
6448 if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) {
6449 return;
6450 }
6451 if ($post->post_status !== 'publish') {
6452 return;
6453 }
6454 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
6455 return;
6456 }
6457
6458 // Auto-submit to Google Instant Indexing
6459 $seo_controls = Metasync::get_option('seo_controls');
6460 if (!empty($seo_controls['enable_googleinstantindex']) && $seo_controls['enable_googleinstantindex'] === 'true') {
6461 $options = get_option('metasync_options_instant_indexing', ['post_types' => []]);
6462 $post_types = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : [];
6463
6464 if (in_array($post->post_type, $post_types) && function_exists('google_index_direct')) {
6465 $service_info = google_index_direct()->get_service_account_info();
6466 if (!isset($service_info['error'])) {
6467 google_index_direct()->index_post($post_id, $post->post_type, 'update');
6468 }
6469 }
6470 }
6471
6472 // Auto-submit to Bing Instant Indexing
6473 require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php';
6474 $bing_instant_index = new Metasync_Bing_Instant_Index();
6475 $bing_instant_index->auto_submit_on_publish($post_id, $post, $update);
6476 }
6477 }
6478