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

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