PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.6
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.6
2.7.1 2.7.2 2.7.0 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 All 141 releases
metasync / admin / class-metasync-admin.php

class-metasync-admin.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.6, at admin/class-metasync-admin.php

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