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

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