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

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

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