PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.9
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.9
2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.5.23 All 138 releases
metasync / admin / class-metasync-admin.php

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

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