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

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

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