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

3,991 lines 141.0 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
47 /**
48 * The ID of this plugin.
49 *
50 * @since 1.0.0
51 * @access private
52 * @var string $plugin_name The ID of this plugin.
53 */
54 private $plugin_name;
55
56 /**
57 * The version of this plugin.
58 *
59 * @since 1.0.0
60 * @access private
61 * @var string $version The current version of this plugin.
62 */
63 private $version;
64
65 /**
66 * Holds the values to be used in the fields callbacks
67 */
68 private $options;
69 public $menu_title = "Search Atlas"; // Default, overridden by get_effective_menu_title()
70 public const page_title = "MetaSync Settings";
71
72 /**
73 * Get effective menu title with whitelabel company name
74 */
75 public function get_effective_menu_title()
76 {
77 $whitelabel_company_name = Metasync::get_whitelabel_company_name();
78
79 if ($whitelabel_company_name) {
80 return $whitelabel_company_name . ' SEO';
81 }
82
83 // Use centralized method for getting effective plugin name
84 return Metasync::get_effective_plugin_name();
85 }
86 public const option_group = "metasync_group";
87 public const option_key = "metasync_options";
88 public static $page_slug = "searchatlas";
89 /**
90 * Get the effective dashboard domain
91 * Returns whitelabel domain if set, otherwise returns default production domain
92 * Uses centralized constant from main Metasync class
93 */
94 public static function get_effective_dashboard_domain()
95 {
96 // Delegate to main Metasync class for consistent domain resolution
97 return Metasync::get_dashboard_domain();
98 }
99
100 /**
101 * Static method to render header and navigation for external pages (like AI Agent)
102 * This creates a minimal instance just for rendering
103 */
104 public static function render_standard_header_nav($page_title = null, $current_page = null) {
105 Metasync_Admin_Navigation::instance()->render_standard_header_nav($page_title, $current_page);
106 }
107
108 /**
109 * Static header render
110 */
111 public static function render_static_header($page_title = null) {
112 Metasync_Admin_Navigation::instance()->render_static_header($page_title);
113 }
114
115 /**
116 * Static navigation render
117 */
118 public static function render_static_navigation($current_page = null) {
119 Metasync_Admin_Navigation::instance()->render_static_navigation($current_page);
120 }
121
122 /**
123 * Get dashboard URL with authentication tokens and tracking parameters
124 * Returns the complete URL for accessing the Search Atlas dashboard
125 */
126 public function get_dashboard_url()
127 {
128 // Get the effective dashboard domain (whitelabel or production)
129 $dashboard_url = self::get_effective_dashboard_domain();
130
131 // Get current options for token inclusion
132 $general_options = Metasync::get_option('general');
133
134 // Add JWT token if available for seamless login
135 if (isset($general_options['linkgraph_token']) && !empty($general_options['linkgraph_token'])) {
136 $dashboard_url .= '/?jwtToken=' . urlencode($general_options['linkgraph_token']);
137 }
138
139 // Add source tracking parameter
140 $dashboard_url .= (strpos($dashboard_url, '?') !== false ? '&' : '?') . 'source=wordpress-plugin';
141
142 // Add whitelabel identification if in whitelabel mode
143 $whitelabel_company_name = Metasync::get_whitelabel_company_name();
144 if ($whitelabel_company_name) {
145 $dashboard_url .= '&whitelabel=' . urlencode($whitelabel_company_name);
146 }
147
148 return $dashboard_url;
149 }
150
151 public const feature_sections = array(
152 'enable_404monitor' => 'Enable 404 Monitor',
153 'enable_siteverification' => 'Enable Site Verification',
154 'enable_localbusiness' => 'Enable Local Business',
155 'enable_codesnippets' => 'Enable Code Snippets',
156 'enable_googleconsole' => 'Enable Google Console',
157 'enable_optimalsettings' => 'Enable Optimal Settings',
158 'enable_globalsettings' => 'Enable Global Settings',
159 'enable_commonmetastatus' => 'Enable Common Meta Status',
160 'enable_socialmeta' => 'Enable Social Meta',
161 'enable_redirections' => 'Enable Redirections',
162 'enable_errorlogs' => 'Enable Error Logs'
163 );
164
165 private $database;
166 private $db_redirection;
167 public $db_heartbeat_errors;
168 public $setup_wizard;
169
170
171 /**
172 * Initialize the class and set its properties.
173 *
174 * @since 1.0.0
175 * @param string $plugin_name The name of this plugin.
176 * @param string $version The version of this plugin.
177 */
178
179 public function __construct($plugin_name, $version, &$database, $db_redirection, $db_heartbeat_errors) // , $data_error_log_list
180 {
181
182 $this->plugin_name = $plugin_name;
183 $this->version = $version;
184 $this->database = $database;
185 $this->db_redirection = $db_redirection;
186 $this->db_heartbeat_errors = $db_heartbeat_errors;
187 // $this->data_error_log_list = $data_error_log_list;
188
189 // Initialize setup wizard
190 $this->setup_wizard = new Metasync_Setup_Wizard($plugin_name, $version);
191
192 // Wire up the extracted settings-fields singleton
193 Metasync_Settings_Fields::instance()->set_admin_instance($this);
194
195 // Get data first for menu configuration
196 $data = Metasync::get_option('general');
197
198 // Set menu title using the effective title (includes whitelabel company name if available)
199 $this->menu_title = $this->get_effective_menu_title();
200 if(!isset( $data['white_label_plugin_menu_slug'])){
201 self::$page_slug = "searchatlas";
202 }else{
203 self::$page_slug = $data['white_label_plugin_menu_slug']=="" ? "searchatlas":$data['white_label_plugin_menu_slug'];
204 }
205
206 add_action('admin_menu', array($this, 'add_plugin_settings_page'));
207 add_action('admin_menu', array($this, 'add_import_external_data_page'));
208 add_action('admin_init', array($this, 'settings_page_init'));
209 add_filter('all_plugins', array($this,'metasync_plugin_white_label'));
210 add_filter( 'plugin_row_meta',array($this,'metasync_view_detials_url'),10,3);
211
212 // Display transient error/success messages for redirections
213 add_action('admin_notices', array($this, 'display_redirection_messages'));
214
215 // Add custom column for HTML-converted pages
216 add_filter('manage_posts_columns', array($this, 'add_html_converted_column'));
217 add_filter('manage_pages_columns', array($this, 'add_html_converted_column'));
218 add_action('manage_posts_custom_column', array($this, 'render_html_converted_column'), 10, 2);
219 add_action('manage_pages_custom_column', array($this, 'render_html_converted_column'), 10, 2);
220
221 // Add badge to page editor screen
222 add_action('edit_form_after_title', array($this, 'add_editor_source_notice'));
223
224 // Add badge to quick edit panel
225 add_action('quick_edit_custom_box', array($this, 'add_quick_edit_source_display'), 10, 2);
226
227 // Add dashboard widget
228 add_action('wp_dashboard_setup', array($this, 'add_html_pages_dashboard_widget'));
229
230 // Add Search Atlas status to WordPress admin bar (priority 999 to ensure plugin is fully loaded)
231 // Always add the action - the method will check the setting internally
232 add_action('admin_bar_menu', array($this, 'add_searchatlas_admin_bar_status'), 999);
233
234 #add css into admin header for icon image
235
236 add_action('admin_head', array($this,'metasync_admin_icon_style'));
237
238 // Always add admin bar styles - the method will check the setting internally
239 add_action('wp_head', array($this,'metasync_admin_bar_style')); // For frontend admin bar
240 add_action('admin_head', array($this,'metasync_admin_bar_style')); // For backend admin bar
241 // removing this as we don't need it anymore because we are using wp-ajax to implement the white label
242 // add_action('update_option_metasync_options', array($this, 'check_and_redirect_slug'), 10, 3);
243
244 // Sync plugin file headers whenever metasync_options is updated (covers AJAX save, Settings API, import)
245 add_action('update_option_metasync_options', array($this, 'on_options_updated_sync_file_headers'), 10, 2);
246
247 add_action('admin_init', array($this, 'initialize_cookie'));
248 add_action('admin_init', array($this, 'maybe_redirect_to_wizard'));
249
250 // Add admin_post hooks for form submissions (WordPress standard way - no output buffering needed)
251 add_action('admin_post_metasync_clear_all_cache_plugins', array($this, 'handle_clear_all_cache_plugins'));
252 add_action('admin_post_metasync_clear_otto_cache_all', array($this, 'handle_clear_otto_cache_all'));
253 add_action('admin_post_metasync_clear_otto_cache_url', array($this, 'handle_clear_otto_cache_url'));
254 add_action('admin_post_metasync_purge_hosting_cache', array($this, 'handle_purge_hosting_cache'));
255
256 // Add AJAX for saving general settings
257 add_action( 'wp_ajax_meta_sync_save_settings', array($this,'meta_sync_save_settings') );
258
259 // Add AJAX for saving Indexation Control settings
260 add_action( 'wp_ajax_meta_sync_save_seo_controls', array($this,'meta_sync_save_seo_controls') );
261
262 // Add AJAX for saving execution settings
263 add_action( 'wp_ajax_metasync_save_execution_settings', array($this, 'ajax_save_execution_settings') );
264
265 // Add AJAX for saving hosting cache settings
266 add_action( 'wp_ajax_metasync_save_hosting_cache_settings', array($this, 'ajax_save_hosting_cache_settings') );
267
268 // Add AJAX for saving edge cache / CDN settings
269 add_action( 'wp_ajax_metasync_save_edge_cache_settings', array('Metasync_Edge_Cache_Settings', 'ajax_save') );
270
271 // Add AJAX handler for Plugin Auth Token refresh
272 add_action('wp_ajax_refresh_plugin_auth_token', array($this, 'refresh_plugin_auth_token'));
273
274 // Add AJAX handler to get current Plugin Auth Token (for UI updates)
275 add_action('wp_ajax_get_plugin_auth_token', array($this, 'get_plugin_auth_token'));
276
277 // Add AJAX handler for creating redirects from 404 suggestions
278 add_action('wp_ajax_metasync_create_redirect_from_404', array($this, 'ajax_create_redirect_from_404'));
279
280 // Add AJAX handler for updating database structure
281 add_action('wp_ajax_metasync_update_db_structure', array($this, 'ajax_update_db_structure'));
282
283 // Add AJAX handlers for setup wizard
284 add_action('wp_ajax_metasync_save_wizard_progress', array($this, 'ajax_save_wizard_progress'));
285 add_action('wp_ajax_metasync_complete_wizard', array($this, 'ajax_complete_wizard'));
286
287 // Add AJAX handlers for robots.txt
288 add_action('wp_ajax_metasync_validate_robots', array($this, 'ajax_validate_robots'));
289 add_action('wp_ajax_metasync_get_default_robots', array($this, 'ajax_get_default_robots'));
290 add_action('wp_ajax_metasync_preview_robots_backup', array($this, 'ajax_preview_robots_backup'));
291 add_action('wp_ajax_metasync_delete_robots_backup', array($this, 'ajax_delete_robots_backup'));
292 add_action('wp_ajax_metasync_restore_robots_backup', array($this, 'ajax_restore_robots_backup'));
293
294 // Add AJAX handlers for host blocking test
295 add_action('wp_ajax_metasync_test_host_blocking_get', array($this, 'ajax_test_host_blocking_get'));
296 add_action('wp_ajax_metasync_test_host_blocking_post', array($this, 'ajax_test_host_blocking_post'));
297
298 // Add AJAX handlers for OTTO excluded URLs
299 add_action('wp_ajax_metasync_otto_add_excluded_url', array($this, 'ajax_otto_add_excluded_url'));
300 add_action('wp_ajax_metasync_otto_delete_excluded_url', array($this, 'ajax_otto_delete_excluded_url'));
301 add_action('wp_ajax_metasync_burst_ping', array($this, 'ajax_burst_ping'));
302 add_action('wp_ajax_metasync_otto_get_excluded_urls', array($this, 'ajax_otto_get_excluded_urls'));
303 add_action('wp_ajax_metasync_otto_recheck_excluded_url', array($this, 'ajax_otto_recheck_excluded_url'));
304
305 # Add AJAX handler for Mixpanel tracking
306 add_action('wp_ajax_metasync_track_one_click_activation', array($this, 'ajax_track_one_click_activation'));
307
308 # Add AJAX handler for submitting issue reports
309 add_action('wp_ajax_metasync_submit_issue_report', array($this, 'ajax_submit_issue_report'));
310
311 # Add AJAX handlers for support token management
312
313 # Add AJAX handler for theme switcher
314 add_action('wp_ajax_metasync_save_theme', array($this, 'ajax_save_theme'));
315
316 # Add AJAX handler for external data import
317 add_action('wp_ajax_metasync_import_external_data', array($this, 'ajax_import_external_data'));
318
319 # Add AJAX handler for SEO metadata batch import
320 add_action('wp_ajax_metasync_import_seo_metadata', array($this, 'ajax_import_seo_metadata'));
321
322 # Add AJAX handler for password recovery
323 add_action('wp_ajax_metasync_recover_password', array($this, 'ajax_recover_password'));
324
325 # Add AJAX handler for resetting bot statistics
326 add_action('wp_ajax_metasync_reset_bot_stats', array($this, 'ajax_reset_bot_stats'));
327
328 # Add admin-post handler for exporting whitelabel settings (file download)
329 add_action('admin_post_metasync_export_whitelabel_settings', array($this, 'handle_export_whitelabel_settings'));
330
331 # Media Optimization AJAX handlers
332 add_action('wp_ajax_metasync_optimize_single_image', array($this, 'ajax_optimize_single_image'));
333 add_action('wp_ajax_metasync_revert_single_image', array($this, 'ajax_revert_single_image'));
334 add_action('wp_ajax_metasync_start_batch_optimize', array($this, 'ajax_start_batch_optimize'));
335 add_action('wp_ajax_metasync_cancel_batch_optimize', array($this, 'ajax_cancel_batch_optimize'));
336 add_action('wp_ajax_metasync_batch_progress', array($this, 'ajax_batch_progress'));
337 add_action('wp_ajax_metasync_bulk_optimize_selected', array($this, 'ajax_bulk_optimize_selected'));
338 add_action('wp_ajax_metasync_process_batch_tick', array($this, 'ajax_process_batch_tick'));
339 add_action('metasync_media_batch_optimize_cron', array($this, 'handle_media_batch_cron'));
340
341 # Add AJAX handlers for Google Instant Indexing
342 add_action('wp_ajax_send_giapi', array($this, 'ajax_send_giapi'));
343
344 # Add AJAX handlers for Bing Instant Indexing (IndexNow)
345 add_action('wp_ajax_send_bing_indexnow', array($this, 'ajax_send_bing_indexnow'));
346
347 # Add hooks for instant indexing settings saves
348 add_action('admin_init', array($this, 'save_instant_indexing_settings'));
349
350 # Add hooks for instant indexing post actions
351 add_filter('post_row_actions', array($this, 'add_instant_indexing_post_actions'), 10, 2);
352 add_filter('page_row_actions', array($this, 'add_instant_indexing_post_actions'), 10, 2);
353
354 # Add hooks for auto-submission on post publish
355 add_action('save_post', array($this, 'auto_submit_to_instant_indexing'), 10, 3);
356
357 // Add REST API endpoint for ping
358 add_action('rest_api_init', array($this, 'register_ping_rest_endpoint'));
359
360 // Add heartbeat cron functionality
361 add_filter('cron_schedules', array($this, 'add_heartbeat_cron_schedule'));
362 add_action('metasync_heartbeat_cron_check', array($this, 'execute_heartbeat_cron_check'));
363 add_action('metasync_burst_heartbeat', array($this, 'execute_burst_heartbeat'));
364 add_action('metasync_announce_cron', array($this, 'execute_announce_cron'));
365
366 // PR3: Transition to KEY_PENDING when API key is added or rotated
367 add_action('metasync_heartbeat_state_key_pending', array($this, 'set_heartbeat_state_key_pending'));
368
369 // Add transient cleanup cron functionality
370 add_action('metasync_cleanup_transients', array($this, 'execute_transient_cleanup'));
371
372 // Schedule heartbeat cron on plugin load (if not already scheduled)
373 add_action('init', array($this, 'maybe_schedule_heartbeat_cron'));
374
375 // Schedule transient cleanup cron on plugin load
376 add_action('init', array($this, 'maybe_schedule_transient_cleanup_cron'));
377
378 // Pre-SSO announce: rate-limited ping when no API key yet (PR4 - heartbeat reliability)
379 add_action('init', array($this, 'maybe_send_pre_sso_announce'));
380
381 # Schedule hidden post manager cron on plugin load (runs every 7 days)
382 add_action('init', array($this, 'maybe_schedule_hidden_post_check'));
383
384 # Schedule OTTO 404 exclusion recheck (runs daily to recheck URLs excluded 7+ days ago)
385 add_action('init', array($this, 'maybe_schedule_otto_recheck_404_cron'));
386
387 # Schedule support token cleanup cron on plugin load (runs daily)
388
389 // Listen for immediate heartbeat trigger requests from other parts of the plugin
390 add_action('metasync_trigger_immediate_heartbeat', array($this, 'handle_immediate_heartbeat_trigger'));
391
392 // Listen for cron scheduling requests (after Search Atlas connect authentication)
393 add_action('metasync_ensure_heartbeat_cron_scheduled', array($this, 'maybe_schedule_heartbeat_cron'));
394
395 // Action Scheduler configuration filters
396 add_filter('action_scheduler_queue_runner_concurrent_batches', array($this, 'get_action_scheduler_batches'));
397 add_filter('action_scheduler_retention_period', array($this, 'get_action_scheduler_retention_period'));
398
399 // Note: Option change monitoring is now handled by the centralized API Key Monitor class
400 // This provides more comprehensive and intelligent monitoring of API key changes
401
402
403 #--------------------------
404 # we are disabling this code
405 # To prevent enabling debuging on every pluging Update
406 # This causes Issue #102 on gilab
407 #--------------------------
408 #
409 # NOTE:
410 # Do not delete this as we may need it in implementing universal logging
411
412 # add_action('upgrader_process_complete', array($this,'metasync_plugin_updated_action'), 10, 2);
413
414 # Hook into post category creation and update (AFTER they are saved)
415 add_action('saved_term', array($this,'admin_crud_term'), 10, 3);
416
417 # Hook into post category deletion (AFTER it is deleted)
418 // add_action('pre_delete_term', array($this,'admin_delete_term'), 10, 2);
419 # Using delete_category hook which fires AFTER category is fully deleted and provides correct data
420 add_action('delete_category', array($this,'admin_delete_category'), 10, 1);
421
422 }
423 /*
424 This function add css to wp admin header
425 */
426 public function metasync_admin_icon_style(){
427 # Get Metasync Option
428
429 $data= Metasync::get_option('general');
430
431 # Get white label menu slug
432 $menu_slug = empty($data['white_label_plugin_menu_slug']) ? self::$page_slug : $data['white_label_plugin_menu_slug'];
433
434 ?>
435 <style>
436 #toplevel_page_<?php echo esc_attr(str_replace(' ', '-',$menu_slug)); ?> .wp-menu-image.dashicons-before img {
437 width: 36px;
438 height: 34px;
439 padding: 0!important;
440 object-fit: contain;
441 object-position: center;
442 }
443 #toplevel_page_<?php echo esc_attr(str_replace(' ', '-',$menu_slug)); ?> .wp-menu-image img {
444 width: 20px !important;
445 }
446 </style>
447 <?php
448 }
449
450 #---------fixes issue : #95 ----------
451 #This function is to redirect in case client changes slug on fresh install
452 #It is called by the add_option hook
453
454 public function redirect_slug_for_freshinstalls(){
455 #get the db menu slug
456 $plugin_menu_slug = Metasync::get_option('general')['white_label_plugin_menu_slug'] ?? '';
457
458 #check that the slug is set or set it to the defaul class slug usually ('searchatlas')
459 $current_slug = empty($plugin_menu_slug) ? self::$page_slug : $plugin_menu_slug;
460
461 #check if we have the cookie set and check if the slug has changed
462 if (isset($_COOKIE['metasync_previous_slug']) && $_COOKIE['metasync_previous_slug'] !== $current_slug) {
463 # the slug changed so we need to update the cookie
464 setcookie('metasync_previous_slug', $current_slug, time() + 3600, COOKIEPATH, COOKIE_DOMAIN);
465 $_COOKIE['metasync_previous_slug'] = $current_slug;
466
467 #Redirect url to the new slug
468 $redirect_url = admin_url('admin.php?page=' . $current_slug);
469
470 wp_redirect($redirect_url);
471 exit;
472 }
473 }
474
475 public function metasync_plugin_updated_action($upgrader_object, $options){
476 if ($options['action'] == 'update' && $options['type'] == 'plugin') {
477 // List of plugins being updated
478 $updated_plugins = $options['plugins'];
479
480 // Loop through plugins and check if your plugin is updated
481 if (in_array(plugin_basename(dirname(__DIR__) . '/metasync.php'), $updated_plugins, true)) {
482 // Only set debug options if they don't already exist (first install/update)
483 if (get_option('wp_debug_enabled') === false) {
484 update_option('wp_debug_enabled', 'true');
485 }
486 if (get_option('wp_debug_log_enabled') === false) {
487 update_option('wp_debug_log_enabled', 'true');
488 }
489 if (get_option('wp_debug_display_enabled') === false) {
490 update_option('wp_debug_display_enabled', 'false');
491 }
492 }
493 }
494 }
495
496 public function initialize_cookie() {
497 // Check if cookie is already set
498 if (!isset($_COOKIE['metasync_previous_slug'])) {
499 // Only set cookie if headers haven't been sent yet
500 if (!headers_sent()) {
501 $data = Metasync::get_option('general');
502 // Retrieve the current slug
503 $initial_slug = isset($data['white_label_plugin_menu_slug']) ? $data['white_label_plugin_menu_slug'] : self::$page_slug;
504 // Set the cookie
505 setcookie('metasync_previous_slug', $initial_slug, time() + 3600, COOKIEPATH, COOKIE_DOMAIN);
506 }
507 }
508 }
509
510 /**
511 * Redirect to setup wizard on first activation
512 *
513 * @since 1.0.0
514 */
515 public function maybe_redirect_to_wizard() {
516 // Only redirect if wizard should be shown
517 if (get_option('metasync_show_wizard') && !isset($_GET['page'])) {
518 delete_option('metasync_show_wizard');
519
520 // Don't redirect during AJAX, cron, or bulk activation
521 if (wp_doing_ajax() || wp_doing_cron() || isset($_GET['activate-multi'])) {
522 return;
523 }
524
525 // Only redirect if user has access to the plugin
526 if (!Metasync::current_user_has_plugin_access()) {
527 return;
528 }
529
530 wp_safe_redirect(admin_url('admin.php?page=' . self::$page_slug . '-setup-wizard'));
531 exit;
532 }
533 }
534
535 public function metasync_display_error_log() {
536 Metasync_Debug_Manager::instance()->metasync_display_error_log($this);
537 }
538 public function metasync_update_wp_config() {
539 Metasync_Debug_Manager::instance()->metasync_update_wp_config();
540 }
541
542
543 /**
544 * Sync plugin file headers when metasync_options is updated.
545 * This ensures whitelabel values are written to the plugin file header
546 * so they persist even when the plugin is deactivated.
547 *
548 * @param mixed $old_value The old option value.
549 * @param mixed $new_value The new option value.
550 * @since 2.5.0
551 */
552 public function on_options_updated_sync_file_headers($old_value, $new_value)
553 {
554 Metasync_Debug_Manager::instance()->on_options_updated_sync_file_headers($old_value, $new_value);
555 }
556
557 public function check_and_redirect_slug($option, $old_value, $new_value) {
558 // Ensure this hook is only triggered for your specific option group
559
560 if (!isset($option['general'] ) && !isset($option['general']['white_label_plugin_menu_slug'])) {
561
562 return;
563 }
564
565 $new_slug = $new_value['general']['white_label_plugin_menu_slug'] ?? self::$page_slug;
566
567 $old_slug = $old_value['general']['white_label_plugin_menu_slug'] ?? self::$page_slug;
568
569 if ($new_slug !== $old_slug && $old_slug !=='' ){
570 // Set a new cookie
571 setcookie('metasync_previous_slug', $new_slug, time() + 3600, COOKIEPATH, COOKIE_DOMAIN);
572 $_COOKIE['metasync_previous_slug'] = $new_slug;
573 // Redirect to the new slug
574 $redirect_url = admin_url('admin.php?page=' . $old_slug);
575 wp_redirect($redirect_url);
576 exit;
577 }else{
578 self::$page_slug = Metasync::get_option('general')['white_label_plugin_menu_slug']=="" ? "searchatlas":Metasync::get_option('general')['white_label_plugin_menu_slug'];
579 $redirect_url = admin_url('admin.php?page=' . self::$page_slug);
580
581 #add redirection for when the old slug is not defined
582 #this fixes the redirect issue #
583 wp_redirect($redirect_url);
584 exit;
585 }
586 }
587 public function metasync_view_detials_url( $plugin_meta, $plugin_file, $plugin_data ) {
588 $plugin_uri = Metasync::get_option('general')['white_label_plugin_uri'] ?? '';
589 $this_plugin = plugin_basename(dirname(__DIR__) . '/metasync.php');
590 if ($this_plugin === $plugin_file && $plugin_uri !== '') {
591 foreach ($plugin_meta as &$meta) {
592 if (strpos($meta, 'open-plugin-details-modal') !== false) {
593 $meta = sprintf(
594 '<a href="%s" class="thickbox open-plugin-details-modal" aria-label="%s" data-title="%s">%s</a>',
595 add_query_arg('TB_iframe', 'true', $plugin_uri),
596 esc_attr(sprintf(esc_html__('More information about %s'), $plugin_data['Name'])),
597 esc_attr($plugin_data['Name']),
598 esc_html__('View details', 'metasync')
599 );
600 break; // Exit loop after replacing the link
601 }
602 }
603 }
604 return $plugin_meta;
605 }
606
607 public function metasync_plugin_white_label($all_plugins) {
608 $general = Metasync::get_option('general');
609 if (!is_array($general)) {
610 return $all_plugins;
611 }
612
613 $plugin_name = $general['white_label_plugin_name'] ?? '';
614 $plugin_description = $general['white_label_plugin_description'] ?? '';
615 $plugin_author = $general['white_label_plugin_author'] ?? '';
616 $plugin_author_uri = $general['white_label_plugin_author_uri'] ?? '';
617 $plugin_uri = $general['white_label_plugin_uri'] ?? '';
618
619 // Dynamically resolve the plugin basename to handle renamed plugin folders
620 $this_plugin = plugin_basename(dirname(__DIR__) . '/metasync.php');
621
622 if (isset($all_plugins[$this_plugin])) {
623 if ($plugin_name !== '') {
624 $all_plugins[$this_plugin]['Name'] = $plugin_name;
625 $all_plugins[$this_plugin]['Title'] = $plugin_name;
626 }
627 if ($plugin_description !== '') {
628 $all_plugins[$this_plugin]['Description'] = $plugin_description;
629 }
630 if ($plugin_author !== '') {
631 $all_plugins[$this_plugin]['Author'] = $plugin_author;
632 $all_plugins[$this_plugin]['AuthorName'] = $plugin_author;
633 }
634 if ($plugin_author_uri !== '') {
635 $all_plugins[$this_plugin]['AuthorURI'] = $plugin_author_uri;
636 }
637 if ($plugin_uri !== '') {
638 $all_plugins[$this_plugin]['PluginURI'] = $plugin_uri;
639 }
640 }
641
642 return $all_plugins;
643 }
644
645 /**
646 * Register the stylesheets for the admin area.
647 *
648 * @since 1.0.0
649 */
650 public function enqueue_styles()
651 {
652
653 wp_enqueue_style(
654 $this->plugin_name,
655 plugin_dir_url(__FILE__) . 'css/metasync-admin.css',
656 array(),
657 $this->version,
658 'all'
659 );
660
661 // Enqueue dashboard-style CSS for admin pages
662 wp_enqueue_style(
663 $this->plugin_name . '-dashboard',
664 plugin_dir_url(__FILE__) . 'css/metasync-dashboard.css',
665 array($this->plugin_name),
666 $this->version,
667 'all'
668 );
669
670 // Enqueue wizard CSS if on wizard page
671 if (isset($_GET['page']) && strpos($_GET['page'], '-setup-wizard') !== false) {
672 wp_enqueue_style(
673 $this->plugin_name . '-setup-wizard',
674 plugin_dir_url(__FILE__) . 'css/metasync-setup-wizard.css',
675 array($this->plugin_name . '-dashboard'),
676 $this->version,
677 'all'
678 );
679 }
680 }
681
682 /**
683 * Register the JavaScript for the admin area.
684 *
685 * @since 1.0.0
686 */
687 public function enqueue_scripts()
688 {
689
690 wp_enqueue_media();
691
692 wp_enqueue_script(
693 $this->plugin_name,
694 plugin_dir_url(__FILE__) . 'js/metasync-admin.js',
695 array('jquery'),
696 $this->version,
697 false
698 );
699
700 // Enqueue dashboard-style JavaScript for enhanced interactions
701 wp_enqueue_script(
702 $this->plugin_name . '-dashboard',
703 plugin_dir_url(__FILE__) . 'js/metasync-dashboard.js',
704 array('jquery', $this->plugin_name),
705 $this->version,
706 true
707 );
708
709 # Enqueue theme switcher
710 wp_enqueue_script(
711 $this->plugin_name . '-theme-switcher',
712 plugin_dir_url(__FILE__) . 'js/metasync-theme-switcher.js',
713 array('jquery'),
714 $this->version,
715 true
716 );
717
718 // --- Phase 5 (#887): Extracted inline JS files ---
719 $current_page = isset($_GET['page']) ? sanitize_text_field($_GET['page']) : '';
720 $plugin_root_url = plugin_dir_url(dirname(__FILE__));
721
722 // Dashboard iframe height (only on dashboard page)
723 if ($current_page === self::$page_slug . '-dashboard' || $current_page === self::$page_slug) {
724 wp_enqueue_script(
725 $this->plugin_name . '-iframe',
726 plugin_dir_url(__FILE__) . 'js/metasync-iframe.js',
727 array(),
728 $this->version,
729 false // Load in head — needed before iframe renders
730 );
731 }
732
733 // Settings page scripts (save btn, clear settings, bing key, access roles)
734 if ($current_page === self::$page_slug || strpos($current_page, self::$page_slug) === 0) {
735 wp_enqueue_script(
736 $this->plugin_name . '-settings',
737 plugin_dir_url(__FILE__) . 'js/metasync-settings.js',
738 array('jquery'),
739 $this->version,
740 true
741 );
742 }
743
744 // Tab switcher (redirections / 404-monitor page)
745 if ($current_page === self::$page_slug . '-redirections') {
746 wp_enqueue_script(
747 $this->plugin_name . '-tab-switcher',
748 plugin_dir_url(__FILE__) . 'js/metasync-tab-switcher.js',
749 array('jquery'),
750 $this->version,
751 true
752 );
753 }
754
755 // Error logs — copy to clipboard
756 if ($current_page === self::$page_slug || strpos($current_page, self::$page_slug) === 0) {
757 wp_enqueue_script(
758 $this->plugin_name . '-error-logs',
759 $plugin_root_url . 'site-error-logs/js/metasync-error-logs.js',
760 array(),
761 $this->version,
762 true
763 );
764 }
765
766 // Access control UI
767 if ($current_page === self::$page_slug || strpos($current_page, self::$page_slug) === 0) {
768 wp_enqueue_script(
769 $this->plugin_name . '-access-control',
770 $plugin_root_url . 'includes/js/metasync-access-control.js',
771 array(),
772 $this->version,
773 true
774 );
775 }
776
777 // 404 monitor filter
778 if ($current_page === self::$page_slug . '-redirections' || $current_page === self::$page_slug . '-404-monitor') {
779 wp_enqueue_script(
780 $this->plugin_name . '-404-monitor',
781 $plugin_root_url . 'views/js/metasync-404-monitor.js',
782 array(),
783 $this->version,
784 true
785 );
786 }
787
788 // Redirections filter
789 if ($current_page === self::$page_slug . '-redirections') {
790 wp_enqueue_script(
791 $this->plugin_name . '-redirections',
792 $plugin_root_url . 'views/js/metasync-redirections.js',
793 array(),
794 $this->version,
795 true
796 );
797 }
798 // --- Phase 5 Part B: Extracted inline JS with wp_localize_script ---
799
800 // Navigation portal menus (top bar + settings inner page)
801 $whitelabel_data = Metasync::get_option('whitelabel');
802 if ($current_page === self::$page_slug || strpos($current_page, self::$page_slug) === 0) {
803 wp_enqueue_script(
804 $this->plugin_name . '-navigation',
805 plugin_dir_url(__FILE__) . 'js/metasync-navigation.js',
806 array(),
807 $this->version,
808 false // Load in head — global functions called from onclick attributes
809 );
810 wp_localize_script($this->plugin_name . '-navigation', 'metasyncNavData', array(
811 'hideAdvanced' => !empty($whitelabel_data['hide_advanced']),
812 'showGeneral' => Metasync_Access_Control::user_can_access('hide_settings'),
813 'pageSlug' => self::$page_slug,
814 ));
815 }
816
817 // Debug mode timer
818 if ($current_page === self::$page_slug || strpos($current_page, self::$page_slug) === 0) {
819 $debug_manager = Metasync_Debug_Mode_Manager::get_instance();
820 $debug_status = $debug_manager->get_status();
821 wp_enqueue_script(
822 $this->plugin_name . '-debug-mode',
823 plugin_dir_url(__FILE__) . 'js/metasync-debug-mode.js',
824 array('jquery'),
825 $this->version,
826 true
827 );
828 wp_localize_script($this->plugin_name . '-debug-mode', 'metasyncDebugData', array(
829 'enabled' => !empty($debug_status['enabled']),
830 'indefinite' => !empty($debug_status['indefinite']),
831 'timeRemaining' => isset($debug_status['time_remaining']) ? (int) $debug_status['time_remaining'] : 0,
832 'statusUrl' => rest_url('metasync/v1/debug-mode/status'),
833 'restNonce' => wp_create_nonce('wp_rest'),
834 ));
835 }
836
837 // MetasyncConfig + admin bar sync
838 if ($current_page === self::$page_slug || strpos($current_page, self::$page_slug) === 0) {
839 wp_enqueue_script(
840 $this->plugin_name . '-config',
841 plugin_dir_url(__FILE__) . 'js/metasync-config.js',
842 array('jquery'),
843 $this->version,
844 true
845 );
846 wp_localize_script($this->plugin_name . '-config', 'metasyncConfigData', array(
847 'pluginName' => Metasync::get_effective_plugin_name(),
848 'ottoName' => Metasync::get_whitelabel_otto_name(),
849 ));
850 }
851
852 // Whitelabel password (forgot password handler)
853 if ($current_page === self::$page_slug || strpos($current_page, self::$page_slug) === 0) {
854 wp_enqueue_script(
855 $this->plugin_name . '-whitelabel',
856 plugin_dir_url(__FILE__) . 'js/metasync-whitelabel.js',
857 array('jquery'),
858 $this->version,
859 true
860 );
861 wp_localize_script($this->plugin_name . '-whitelabel', 'metasyncWhitelabelData', array(
862 'recoverNonce' => wp_create_nonce('metasync_recover_password_nonce'),
863 ));
864 }
865
866 // Whitelabel connect (validation modal + lock section + export)
867 if ($current_page === self::$page_slug || strpos($current_page, self::$page_slug) === 0) {
868 wp_enqueue_script(
869 $this->plugin_name . '-connect',
870 plugin_dir_url(__FILE__) . 'js/metasync-connect.js',
871 array('jquery'),
872 $this->version,
873 true
874 );
875 wp_localize_script($this->plugin_name . '-connect', 'metasyncConnectData', array(
876 'optionKey' => self::option_key,
877 'storedPassword' => isset($whitelabel_data['settings_password']) ? $whitelabel_data['settings_password'] : '',
878 'adminPostUrl' => admin_url('admin-post.php'),
879 'exportNonce' => wp_create_nonce('metasync_export_whitelabel'),
880 'ajaxUrl' => admin_url('admin-ajax.php'),
881 'logoutNonceField' => wp_nonce_field('whitelabel_logout_nonce', 'whitelabel_logout_nonce', true, false),
882 ));
883 }
884
885 // Host blocking test (settings + dashboard)
886 if ($current_page === self::$page_slug || strpos($current_page, self::$page_slug) === 0) {
887 wp_enqueue_script(
888 $this->plugin_name . '-host-blocking',
889 plugin_dir_url(__FILE__) . 'js/metasync-host-blocking.js',
890 array('jquery'),
891 $this->version,
892 true
893 );
894 wp_localize_script($this->plugin_name . '-host-blocking', 'metasyncHostBlockingData', array(
895 'ajaxUrl' => admin_url('admin-ajax.php'),
896 ));
897 }
898
899 // OTTO excluded URLs
900 if ($current_page === self::$page_slug || strpos($current_page, self::$page_slug) === 0) {
901 wp_enqueue_script(
902 $this->plugin_name . '-excluded-urls',
903 plugin_dir_url(__FILE__) . 'js/metasync-excluded-urls.js',
904 array('jquery'),
905 $this->version,
906 true
907 );
908 wp_localize_script($this->plugin_name . '-excluded-urls', 'metasyncExcludedUrlsData', array(
909 'ajaxUrl' => admin_url('admin-ajax.php'),
910 'nonce' => wp_create_nonce('metasync_otto_excluded_urls'),
911 ));
912 }
913
914 // Execution settings form
915 if ($current_page === self::$page_slug || strpos($current_page, self::$page_slug) === 0) {
916 $server_limits = $this->get_server_limits();
917 wp_enqueue_script(
918 $this->plugin_name . '-execution-settings',
919 plugin_dir_url(__FILE__) . 'js/metasync-execution-settings.js',
920 array('jquery'),
921 $this->version,
922 true
923 );
924 wp_localize_script($this->plugin_name . '-execution-settings', 'metasyncExecSettingsData', array(
925 'serverMaxExecTime' => ($server_limits['max_execution_time_raw'] == -1) ? 'Infinity' : (int) $server_limits['max_execution_time_raw'],
926 'serverMaxMemory' => ($server_limits['memory_limit_raw'] == -1) ? 'Infinity' : (int) $server_limits['memory_limit_raw'],
927 'canChangeMemory' => !empty($server_limits['can_change_memory']),
928 ));
929 }
930
931 // Quick edit badge (custom pages)
932 wp_enqueue_script(
933 $this->plugin_name . '-quick-edit',
934 plugin_dir_url(__FILE__) . 'js/metasync-quick-edit.js',
935 array('jquery'),
936 $this->version,
937 true
938 );
939 wp_localize_script($this->plugin_name . '-quick-edit', 'metasyncQuickEditData', array(
940 'standardPageLabel' => __('Standard page', 'metasync'),
941 ));
942
943 // Report issue form
944 if ($current_page === self::$page_slug . '-report-issue') {
945 wp_enqueue_script(
946 $this->plugin_name . '-report-issue',
947 $plugin_root_url . 'views/js/metasync-report-issue.js',
948 array('jquery'),
949 $this->version,
950 true
951 );
952 }
953
954 // Bing console
955 if ($current_page === self::$page_slug . '-bing-console') {
956 wp_enqueue_script(
957 $this->plugin_name . '-bing-console',
958 $plugin_root_url . 'views/js/metasync-bing-console.js',
959 array('jquery'),
960 $this->version,
961 true
962 );
963 }
964
965 // Add redirection form
966 if ($current_page === self::$page_slug . '-redirections') {
967 wp_enqueue_script(
968 $this->plugin_name . '-add-redirection',
969 $plugin_root_url . 'views/js/metasync-add-redirection.js',
970 array(),
971 $this->version,
972 true
973 );
974 }
975
976 // Import redirections
977 if ($current_page === self::$page_slug . '-redirections') {
978 wp_enqueue_script(
979 $this->plugin_name . '-import-redirections',
980 $plugin_root_url . 'views/js/metasync-import-redirections.js',
981 array('jquery'),
982 $this->version,
983 true
984 );
985 wp_localize_script($this->plugin_name . '-import-redirections', 'metasyncImportRedirData', array(
986 'nonce' => wp_create_nonce('metasync_import_redirections'),
987 'redirectUrl' => admin_url('admin.php?page=' . self::$page_slug . '-redirections'),
988 ));
989 }
990
991 // Import external data (SEO metadata)
992 if ($current_page === self::$page_slug || strpos($current_page, self::$page_slug) === 0) {
993 wp_enqueue_script(
994 $this->plugin_name . '-import-external-data',
995 $plugin_root_url . 'views/js/metasync-import-external-data.js',
996 array('jquery'),
997 $this->version,
998 true
999 );
1000 wp_localize_script($this->plugin_name . '-import-external-data', 'metasyncImportData', array(
1001 'importNonce' => wp_create_nonce('metasync_import_external_data'),
1002 'seoImportNonce' => wp_create_nonce('metasync_import_seo_metadata'),
1003 ));
1004 }
1005
1006 // OTTO bot statistics
1007 if ($current_page === self::$page_slug || strpos($current_page, self::$page_slug) === 0) {
1008 wp_enqueue_script(
1009 $this->plugin_name . '-bot-statistics',
1010 $plugin_root_url . 'views/js/metasync-bot-statistics.js',
1011 array('jquery'),
1012 $this->version,
1013 true
1014 );
1015 wp_localize_script($this->plugin_name . '-bot-statistics', 'metasyncBotStatsData', array(
1016 'resetNonce' => wp_create_nonce('metasync_reset_bot_stats'),
1017 ));
1018 }
1019
1020 // OTTO debug page
1021 if ($current_page === self::$page_slug . '-otto-debug') {
1022 wp_enqueue_script(
1023 $this->plugin_name . '-otto-debug',
1024 plugin_dir_url(__FILE__) . 'js/metasync-otto-debug.js',
1025 array('jquery'),
1026 $this->version,
1027 true
1028 );
1029 wp_localize_script($this->plugin_name . '-otto-debug', 'metasyncOttoDebugData', array(
1030 'nonce' => wp_create_nonce('metasync_otto_debug'),
1031 ));
1032 }
1033
1034 // --- End Phase 5 enqueues ---
1035
1036 # Localize theme switcher script
1037 wp_localize_script(
1038 $this->plugin_name . '-theme-switcher',
1039 'metasyncThemeData',
1040 array(
1041 'ajaxUrl' => admin_url('admin-ajax.php'),
1042 'nonce' => wp_create_nonce('metasync_theme_nonce'),
1043 'currentTheme' => get_option('metasync_theme', 'dark')
1044 )
1045 );
1046
1047 // Localize the script to make the AJAX URL accessible
1048 $options = Metasync::get_option('general');
1049 // Get connection status for JavaScript
1050 $general_settings = Metasync::get_option('general');
1051 $searchatlas_api_key = isset($general_settings['searchatlas_api_key']) ? $general_settings['searchatlas_api_key'] : '';
1052 $otto_pixel_uuid = isset($general_settings['otto_pixel_uuid']) ? $general_settings['otto_pixel_uuid'] : '';
1053
1054 // SECURITY FIX (CVE-2025-14386): Only generate Search Atlas connect nonce for administrators
1055 // Using strict capability check instead of plugin access roles
1056 $sa_connect_nonce = '';
1057 if (current_user_can('manage_options')) {
1058 $sa_connect_nonce = wp_create_nonce('metasync_sa_connect_nonce');
1059 }
1060
1061 $heartbeat_state = $this->get_heartbeat_state();
1062 wp_localize_script( $this->plugin_name, 'metaSync', array(
1063 'ajax_url' => admin_url( 'admin-ajax.php' ),
1064 'admin_url'=>admin_url('admin.php'),
1065 'sa_connect_nonce' => $sa_connect_nonce,
1066 'reset_auth_nonce' => wp_create_nonce('metasync_reset_auth_nonce'),
1067 'burst_ping_nonce' => wp_create_nonce('metasync_burst_ping'),
1068 'heartbeat_state' => $heartbeat_state,
1069 'dashboard_domain' => self::get_effective_dashboard_domain(),
1070 'support_email' => Metasync::SUPPORT_EMAIL,
1071 'documentation_domain' => Metasync::DOCUMENTATION_DOMAIN,
1072 'debug_enabled' => WP_DEBUG || (defined('METASYNC_DEBUG') && constant('METASYNC_DEBUG')),
1073 'searchatlas_api_key' => !empty($searchatlas_api_key),
1074 'otto_pixel_uuid' => $otto_pixel_uuid,
1075 'is_connected' => (bool)$this->is_heartbeat_connected()
1076 ));
1077
1078 // Ensure ajaxurl is available for admin pages (WordPress standard)
1079 // This creates a global ajaxurl variable for JavaScript
1080 wp_enqueue_script('wp-util');
1081
1082 // Add inline script to ensure ajaxurl is defined
1083 $inline_script = "
1084 if (typeof ajaxurl === 'undefined') {
1085 var ajaxurl = '" . esc_js(admin_url('admin-ajax.php')) . "';
1086 }
1087
1088 // Add Plugin Auth Token refresh functionality
1089 jQuery(document).ready(function($) {
1090 $('#refresh-plugin-auth-token').click(function() {
1091 var button = $(this);
1092 var originalText = button.text();
1093
1094 if (confirm('Are you sure you want to refresh the Plugin Auth Token? This will generate a new token and update the heartbeat API.')) {
1095 // Disable button and show loading
1096 button.prop('disabled', true).text('🔄 Refreshing...');
1097
1098 $.post(ajaxurl, {
1099 action: 'refresh_plugin_auth_token',
1100 nonce: '" . wp_create_nonce('refresh_plugin_auth_token') . "'
1101 })
1102 .done(function(response) {
1103 if (response.success && response.data && response.data.new_token) {
1104 // Update the field value immediately
1105 $('#apikey').val(response.data.new_token);
1106
1107 // Visual feedback with green border
1108 $('#apikey').css('border', '2px solid #28a745').animate({borderColor: '#ddd'}, 2000);
1109
1110 alert('�
1111 Plugin Auth Token refreshed successfully!\\n\\nNew token: ' + response.data.new_token.substring(0, 8) + '...');
1112 } else {
1113 alert('❌ Error refreshing token: ' + (response.data ? response.data.message : 'Unknown error'));
1114 }
1115 })
1116 .fail(function() {
1117 alert('❌ Network error while refreshing token');
1118 })
1119 .always(function() {
1120 // Re-enable button
1121 button.prop('disabled', false).text(originalText);
1122 });
1123 }
1124 });
1125 });
1126 ";
1127 wp_add_inline_script($this->plugin_name, $inline_script);
1128 add_action('admin_notices', array($this, 'permalink_structure_dashboard_warning'));
1129 // Display update warning banner if plugin update is available
1130 add_action('admin_notices', array($this, 'display_update_warning_banner'));
1131 // Enqueue wizard assets if on wizard page
1132 if (isset($_GET['page']) && strpos($_GET['page'], '-setup-wizard') !== false) {
1133 wp_enqueue_script(
1134 $this->plugin_name . '-setup-wizard',
1135 plugin_dir_url(__FILE__) . 'js/metasync-setup-wizard.js',
1136 array('jquery'),
1137 $this->version,
1138 true
1139 );
1140
1141 wp_localize_script($this->plugin_name . '-setup-wizard', 'metasyncWizardData', array(
1142 'nonce' => wp_create_nonce('metasync_wizard'),
1143 'ssoNonce' => wp_create_nonce('metasync_sso_nonce'),
1144 'saConnectNonce' => wp_create_nonce('metasync_sa_connect_nonce'),
1145 'importNonce' => wp_create_nonce('metasync_import_external_data'),
1146 'dashboardUrl' => admin_url('admin.php?page=' . self::$page_slug . '-dashboard'),
1147 'currentStep' => 1,
1148 'totalSteps' => 6,
1149 'pluginName' => Metasync::get_effective_plugin_name()
1150 ));
1151 }
1152
1153 wp_enqueue_script('heartbeat');
1154 }
1155
1156 /**
1157 * Settings of HeartBeat API for admin area.
1158 * Set time interval of send request.
1159 */
1160 function metasync_heartbeat_settings($settings)
1161 {
1162 global $heartbeat_frequency;
1163 $settings['interval'] = 300;
1164 return $settings;
1165 }
1166
1167 /**
1168 * Data or Response received from HeartBeat API for admin area.
1169 */
1170 function metasync_received_data($response, $data)
1171 {
1172 // if ($data['client'] == 'marco')
1173
1174 $response['server'] = wp_json_encode($data);
1175
1176 return $response;
1177 }
1178
1179 /**
1180 * Add Import External Data page
1181 */
1182 public function add_import_external_data_page()
1183 {
1184 # Check if current user has plugin access based on role settings
1185 if (!$this->current_user_has_plugin_access()) {
1186 return; // Don't add this page for users without access
1187 }
1188
1189 // Use 'read' capability since actual access is controlled by current_user_has_plugin_access() check above
1190 add_submenu_page(
1191 null, // Hidden from menu, linked from other pages
1192 'Import External Data',
1193 'Import External Data',
1194 'read',
1195 'metasync-import-external',
1196 array($this, 'render_import_external_data_page')
1197 );
1198 }
1199
1200 /**
1201 * Render Import External Data page
1202 */
1203 public function render_import_external_data_page()
1204 {
1205 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-external-importer.php';
1206 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-import-external-data.php';
1207 }
1208
1209 /**
1210 * AJAX handler for external data import
1211 */
1212 public function ajax_import_external_data()
1213 {
1214 Metasync_Admin_Ajax::instance()->ajax_import_external_data();
1215 }
1216
1217 /**
1218 * AJAX handler for SEO metadata batch import
1219 * Supports batch processing with progress tracking
1220 */
1221 public function ajax_import_seo_metadata()
1222 {
1223 Metasync_Admin_Ajax::instance()->ajax_import_seo_metadata();
1224 }
1225
1226 /**
1227 * Additional context validation for Search Atlas connect tokens.
1228 *
1229 * Validates site URL and optional IP/user-agent context for connect tokens
1230 * used during the Search Atlas API key + Otto UUID retrieval flow.
1231 * This does NOT create WordPress login sessions.
1232 */
1233 private function validate_searchatlas_context($token_data)
1234 {
1235 return Metasync_Connect_Manager::instance()->validate_searchatlas_context($token_data);
1236 }
1237
1238 private function should_validate_ip()
1239 {
1240 return Metasync_Connect_Manager::instance()->should_validate_ip();
1241 }
1242
1243 private function are_user_agents_incompatible($old_ua, $new_ua)
1244 {
1245 return Metasync_Connect_Manager::instance()->are_user_agents_incompatible($old_ua, $new_ua);
1246 }
1247
1248 private function extract_browser_name($ua)
1249 {
1250 return Metasync_Connect_Manager::instance()->extract_browser_name($ua);
1251 }
1252
1253 public function generate_searchatlas_wp_connect_token($regenerate = false)
1254 {
1255 return Metasync_Connect_Manager::instance()->generate_searchatlas_wp_connect_token($regenerate);
1256 }
1257
1258 private function ensure_plugin_auth_token_exists()
1259 {
1260 Metasync_Connect_Manager::instance()->ensure_plugin_auth_token_exists();
1261 }
1262
1263 public function generate_searchatlas_connect_url()
1264 {
1265 Metasync_Connect_Manager::instance()->generate_searchatlas_connect_url();
1266 }
1267
1268 public function check_searchatlas_connect_status()
1269 {
1270 Metasync_Connect_Manager::instance()->check_searchatlas_connect_status();
1271 }
1272
1273 private function create_searchatlas_nonce_token()
1274 {
1275 return Metasync_Connect_Manager::instance()->create_searchatlas_nonce_token();
1276 }
1277
1278 private function get_client_ip()
1279 {
1280 return Metasync_Connect_Manager::instance()->get_client_ip();
1281 }
1282
1283 private function create_encrypted_searchatlas_token($metadata = array())
1284 {
1285 return Metasync_Connect_Manager::instance()->create_encrypted_searchatlas_token($metadata);
1286 }
1287
1288 private function wp_encrypt_token($payload)
1289 {
1290 return Metasync_Connect_Manager::instance()->wp_encrypt_token($payload);
1291 }
1292
1293 public function test_enhanced_searchatlas_tokens()
1294 {
1295 return Metasync_Connect_Manager::instance()->test_enhanced_searchatlas_tokens();
1296 }
1297
1298 public function test_searchatlas_ajax_endpoint()
1299 {
1300 Metasync_Connect_Manager::instance()->test_searchatlas_ajax_endpoint();
1301 }
1302
1303 public function simple_ajax_test()
1304 {
1305 Metasync_Connect_Manager::instance()->simple_ajax_test();
1306 }
1307
1308 /**
1309 * Create Admin Dashboard Iframe Page
1310 * Embeds the Search Atlas dashboard directly in WordPress admin
1311 */
1312 public function create_admin_dashboard_iframe()
1313 {
1314 Metasync_Admin_Pages::get_instance($this)->create_admin_dashboard_iframe();
1315 }
1316
1317 /**
1318 * Render OTTO cache management interface
1319 */
1320 public function render_otto_cache_management()
1321 {
1322 Metasync_Otto_Cache_Manager::instance()->render_otto_cache_management();
1323 }
1324
1325 /**
1326 * Render debug mode section for inclusion in Advanced settings
1327 */
1328 public function render_debug_mode_section()
1329 {
1330 Metasync_Debug_Manager::instance()->render_debug_mode_section();
1331 }
1332
1333 /**
1334 * Render error log content for inclusion in Advanced settings
1335 */
1336 public function render_error_log_content()
1337 {
1338 Metasync_Debug_Manager::instance()->render_error_log_content();
1339 }
1340
1341 /**
1342 * WordPress standard handler for clearing all cache plugins (admin_post hook)
1343 * This method runs early and prevents any output before redirect
1344 */
1345 public function handle_clear_all_cache_plugins() {
1346 Metasync_Otto_Cache_Manager::instance()->handle_clear_all_cache_plugins();
1347 }
1348
1349 /**
1350 * WordPress standard handler for clearing OTTO cache (admin_post hook)
1351 */
1352 public function handle_clear_otto_cache_all() {
1353 Metasync_Otto_Cache_Manager::instance()->handle_clear_otto_cache_all();
1354 }
1355
1356 /**
1357 * WordPress standard handler for clearing OTTO cache by URL (admin_post hook)
1358 */
1359 public function handle_clear_otto_cache_url() {
1360 Metasync_Otto_Cache_Manager::instance()->handle_clear_otto_cache_url();
1361 }
1362
1363 /**
1364 * Get hosting cache integration settings with defaults
1365 *
1366 * @return array Settings array with 'wpengine_enabled' and 'kinsta_enabled' keys
1367 */
1368 private function get_hosting_cache_settings() {
1369 $defaults = array(
1370 'wpengine_enabled' => true,
1371 'kinsta_enabled' => true,
1372 );
1373 $saved = get_option('metasync_hosting_cache_options', array());
1374 return wp_parse_args($saved, $defaults);
1375 }
1376
1377 /**
1378 * AJAX handler for saving hosting cache settings
1379 */
1380 public function ajax_save_hosting_cache_settings() {
1381 if (!isset($_POST['hosting_cache_nonce']) || !wp_verify_nonce($_POST['hosting_cache_nonce'], 'metasync_hosting_cache_nonce')) {
1382 wp_send_json_error(array('message' => 'Security check failed'));
1383 return;
1384 }
1385
1386 if (!Metasync::current_user_has_plugin_access()) {
1387 wp_send_json_error(array('message' => 'Insufficient permissions'));
1388 return;
1389 }
1390
1391 $settings = array(
1392 'wpengine_enabled' => !empty($_POST['wpengine_enabled']) && $_POST['wpengine_enabled'] === '1',
1393 'kinsta_enabled' => !empty($_POST['kinsta_enabled']) && $_POST['kinsta_enabled'] === '1',
1394 );
1395
1396 update_option('metasync_hosting_cache_options', $settings);
1397 wp_send_json_success(array('message' => 'Hosting cache settings saved'));
1398 }
1399
1400 /**
1401 * admin_post handler: purge WP Engine and Kinsta hosting-level caches
1402 */
1403 public function handle_purge_hosting_cache() {
1404 if (!isset($_POST['hosting_cache_purge_nonce']) || !wp_verify_nonce($_POST['hosting_cache_purge_nonce'], 'metasync_hosting_cache_purge_nonce')) {
1405 wp_die('Security check failed');
1406 }
1407
1408 if (!Metasync::current_user_has_plugin_access()) {
1409 wp_die('You do not have permission to perform this action');
1410 }
1411
1412 $redirect_url = admin_url('admin.php?page=' . self::$page_slug . '&tab=advanced');
1413 $settings = $this->get_hosting_cache_settings();
1414 $cleared = array();
1415 $failed = array();
1416 $not_detected = array();
1417
1418 // WP Engine native purge (Varnish + Memcached)
1419 if (!empty($settings['wpengine_enabled'])) {
1420 if (class_exists('WpeCommon')) {
1421 try {
1422 WpeCommon::purge_varnish_cache();
1423 WpeCommon::purge_memcached();
1424 $cleared[] = 'WP Engine';
1425 } catch (Exception $e) {
1426 error_log('MetaSync: WP Engine hosting cache purge failed - ' . $e->getMessage());
1427 $failed[] = 'WP Engine';
1428 }
1429 } else {
1430 $not_detected[] = 'WP Engine';
1431 }
1432 }
1433
1434 // Kinsta native purge (full site)
1435 if (!empty($settings['kinsta_enabled'])) {
1436 if (class_exists('KinstaCache')) {
1437 try {
1438 KinstaCache::get_instance()->kinsta_cache_purge_full();
1439 $cleared[] = 'Kinsta';
1440 } catch (Exception $e) {
1441 error_log('MetaSync: Kinsta hosting cache purge failed - ' . $e->getMessage());
1442 $failed[] = 'Kinsta';
1443 }
1444 } else {
1445 $not_detected[] = 'Kinsta';
1446 }
1447 }
1448
1449 $redirect_url .= '&hosting_cache_cleared=1';
1450 if (!empty($cleared)) {
1451 $redirect_url .= '&hc_cleared=' . urlencode(implode(',', $cleared));
1452 }
1453 if (!empty($failed)) {
1454 $redirect_url .= '&hc_failed=' . urlencode(implode(',', $failed));
1455 }
1456 if (!empty($not_detected)) {
1457 $redirect_url .= '&hc_not_detected=' . urlencode(implode(',', $not_detected));
1458 }
1459
1460 wp_safe_redirect($redirect_url);
1461 exit;
1462 }
1463
1464 /**
1465 * Handle debug mode operations (enable/disable/extend)
1466 */
1467 private function handle_debug_mode_operations()
1468 {
1469 Metasync_Debug_Manager::instance()->handle_debug_mode_operations();
1470 }
1471
1472 /**
1473 * Handle error log operations (clear)
1474 */
1475 private function handle_error_log_operations()
1476 {
1477 Metasync_Debug_Manager::instance()->handle_error_log_operations();
1478 }
1479
1480 /**
1481 * Handle clear all settings operations
1482 */
1483 private function handle_clear_all_settings()
1484 {
1485 Metasync_Debug_Manager::instance()->handle_clear_all_settings();
1486 }
1487
1488 /**
1489 * Handle saving plugin access roles from Advanced Settings
1490 * @deprecated Now handled by Metasync_Settings_Registration::settings_page_init()
1491 */
1492 private function handle_plugin_access_roles_save() {
1493 }
1494
1495 /**
1496 * Get error log content for display
1497 */
1498 private function get_error_log_content()
1499 {
1500 return Metasync_Debug_Manager::instance()->get_error_log_content();
1501 }
1502
1503 /**
1504 * Memory-efficient function to get last N lines from a large file
1505 */
1506 private function get_log_tail($file_path, $lines = null)
1507 {
1508 return Metasync_Debug_Manager::instance()->get_log_tail($file_path, $lines);
1509 }
1510
1511 /**
1512 * Test whitelabel domain functionality (development/debugging)
1513 */
1514 public function test_whitelabel_domain()
1515 {
1516 Metasync_Connect_Manager::instance()->test_whitelabel_domain();
1517 }
1518
1519 /**
1520 * Decrypt token using WordPress SALTs
1521 */
1522 private function wp_decrypt_token($encrypted_token)
1523 {
1524 return Metasync_Connect_Manager::instance()->wp_decrypt_token($encrypted_token);
1525 }
1526
1527 /**
1528 * Get active JWT token for the plugin
1529 * Public static method accessible from anywhere in the plugin
1530 *
1531 * @param bool $force_refresh Force generation of new token even if cached one exists
1532 * @return string|false JWT token on success, false on failure
1533 */
1534 public static function get_active_jwt_token($force_refresh = false)
1535 {
1536 return Metasync_Connect_Manager::instance()->get_active_jwt_token($force_refresh);
1537 }
1538
1539
1540 /**
1541 * Get fresh JWT token from Search Atlas API with caching
1542 * Generates and caches JWT tokens to avoid repeated API calls
1543 *
1544 * @return string|false JWT token on success, false on failure
1545 */
1546 public function get_fresh_jwt_token()
1547 {
1548 return Metasync_Connect_Manager::instance()->get_fresh_jwt_token();
1549 }
1550
1551 /**
1552 * Clear cached JWT tokens
1553 * Useful when authentication is reset or API key changes
1554 */
1555 private function clear_jwt_token_cache()
1556 {
1557 Metasync_Connect_Manager::instance()->clear_jwt_token_cache();
1558 }
1559
1560
1561
1562 /**
1563 * Data or Response received from HeartBeat API for admin area.
1564 */
1565 public function lgSendCustomerParams()
1566 {
1567 Metasync_Admin_Ajax::instance()->lgSendCustomerParams();
1568 }
1569
1570
1571
1572 /**
1573 * Add CSS styles for Search Atlas admin bar status indicator
1574 */
1575 public function metasync_admin_bar_style()
1576 {
1577 Metasync_Admin_Navigation::instance()->metasync_admin_bar_style();
1578 }
1579
1580 /**
1581 * Add Search Atlas status indicator to WordPress admin bar
1582 * Shows sync status with green/red emoji
1583 */
1584 public function add_searchatlas_admin_bar_status($wp_admin_bar)
1585 {
1586 Metasync_Admin_Navigation::instance()->add_searchatlas_admin_bar_status($wp_admin_bar);
1587 }
1588
1589 // ------------------------------------------------------------------
1590 // Heartbeat / connection-monitoring – delegated to Metasync_Heartbeat_Manager
1591 // ------------------------------------------------------------------
1592
1593 public function is_heartbeat_connected($general_settings = null)
1594 {
1595 return Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general_settings);
1596 }
1597
1598 public function fetch_public_hash($otto_pixel_uuid, $jwt_token)
1599 {
1600 return Metasync_Heartbeat_Manager::instance()->fetch_public_hash($otto_pixel_uuid, $jwt_token);
1601 }
1602
1603 public function schedule_heartbeat_cron()
1604 {
1605 Metasync_Heartbeat_Manager::instance()->schedule_heartbeat_cron();
1606 }
1607
1608 public function unschedule_heartbeat_cron()
1609 {
1610 Metasync_Heartbeat_Manager::instance()->unschedule_heartbeat_cron();
1611 }
1612
1613 public function execute_heartbeat_cron_check()
1614 {
1615 return Metasync_Heartbeat_Manager::instance()->execute_heartbeat_cron_check();
1616 }
1617
1618 public function add_heartbeat_cron_schedule($schedules)
1619 {
1620 return Metasync_Heartbeat_Manager::instance()->add_heartbeat_cron_schedule($schedules);
1621 }
1622
1623 public function get_heartbeat_state()
1624 {
1625 return Metasync_Heartbeat_Manager::instance()->get_heartbeat_state();
1626 }
1627
1628 public function set_heartbeat_state_key_pending()
1629 {
1630 Metasync_Heartbeat_Manager::instance()->set_heartbeat_state_key_pending();
1631 }
1632
1633 public function execute_burst_heartbeat()
1634 {
1635 Metasync_Heartbeat_Manager::instance()->execute_burst_heartbeat();
1636 }
1637
1638 public function execute_announce_cron()
1639 {
1640 Metasync_Heartbeat_Manager::instance()->execute_announce_cron();
1641 }
1642
1643 public function unschedule_burst_heartbeat_cron()
1644 {
1645 Metasync_Heartbeat_Manager::instance()->unschedule_burst_heartbeat_cron();
1646 }
1647
1648 public function unschedule_announce_cron()
1649 {
1650 Metasync_Heartbeat_Manager::instance()->unschedule_announce_cron();
1651 }
1652
1653 public function maybe_schedule_heartbeat_cron()
1654 {
1655 Metasync_Heartbeat_Manager::instance()->maybe_schedule_heartbeat_cron();
1656 }
1657
1658 public function maybe_send_pre_sso_announce()
1659 {
1660 Metasync_Heartbeat_Manager::instance()->maybe_send_pre_sso_announce();
1661 }
1662
1663 public function trigger_immediate_heartbeat_check($context = 'Manual trigger')
1664 {
1665 return Metasync_Heartbeat_Manager::instance()->trigger_immediate_heartbeat_check($context);
1666 }
1667
1668 public function handle_immediate_heartbeat_trigger($context = 'WordPress action trigger')
1669 {
1670 Metasync_Heartbeat_Manager::instance()->handle_immediate_heartbeat_trigger($context);
1671 }
1672
1673 public function ajax_burst_ping()
1674 {
1675 Metasync_Heartbeat_Manager::instance()->ajax_burst_ping();
1676 }
1677
1678 public function update_heartbeat_cache_after_sync($is_connected, $context = 'Sync operation')
1679 {
1680 return Metasync_Heartbeat_Manager::instance()->update_heartbeat_cache_after_sync($is_connected, $context);
1681 }
1682
1683
1684 /**
1685 * Refresh Plugin Auth Token
1686 * Generates a new Plugin Auth Token and updates heartbeat API
1687 */
1688 public function refresh_plugin_auth_token()
1689 {
1690 Metasync_Connect_Manager::instance()->refresh_plugin_auth_token();
1691 }
1692
1693 public function get_plugin_auth_token()
1694 {
1695 Metasync_Connect_Manager::instance()->get_plugin_auth_token();
1696 }
1697
1698 public function reset_searchatlas_authentication()
1699 {
1700 Metasync_Connect_Manager::instance()->reset_searchatlas_authentication();
1701 }
1702
1703 private function cleanup_searchatlas_nonce_tokens()
1704 {
1705 return Metasync_Connect_Manager::instance()->cleanup_searchatlas_nonce_tokens();
1706 }
1707
1708 private function cleanup_searchatlas_rate_limits()
1709 {
1710 return Metasync_Connect_Manager::instance()->cleanup_searchatlas_rate_limits();
1711 }
1712
1713 private function get_available_menu_items()
1714 {
1715 return Metasync_Admin_Navigation::instance()->get_available_menu_items();
1716 }
1717
1718 /**
1719 * Add options page
1720 */
1721 public function add_plugin_settings_page()
1722 {
1723 Metasync_Admin_Navigation::instance()->add_plugin_settings_page($this);
1724 }
1725
1726 /**
1727 * General Options page callback
1728 */
1729 public function create_admin_settings_page()
1730 {
1731 Metasync_Admin_Pages::get_instance($this)->create_admin_settings_page();
1732 }
1733
1734 public function render_navigation_menu($current_page = null)
1735 {
1736 Metasync_Admin_Navigation::instance()->render_navigation_menu($current_page);
1737 }
1738
1739 public function render_plugin_header($page_title = null)
1740 {
1741 Metasync_Admin_Navigation::instance()->render_plugin_header($page_title);
1742 }
1743
1744 /*
1745 Method to handle Ajax request from "General Settings" page
1746 */
1747 public function meta_sync_save_settings() {
1748 Metasync_Settings_Registration::instance()->meta_sync_save_settings();
1749 }
1750
1751 /**
1752 * AJAX handler for saving execution settings
1753 */
1754 public function ajax_save_execution_settings() {
1755 Metasync_Settings_Registration::instance()->ajax_save_execution_settings();
1756 }
1757
1758 /**
1759 * Get Action Scheduler concurrent batches from execution settings
1760 *
1761 * @param int $default_batches Default concurrent batches
1762 * @return int Configured concurrent batches
1763 */
1764 public function get_action_scheduler_batches($default_batches) {
1765 // Only apply if Action Scheduler is active
1766 if (!class_exists('ActionScheduler')) {
1767 return $default_batches;
1768 }
1769
1770 return $this->get_execution_setting('action_scheduler_batches');
1771 }
1772
1773 /**
1774 * Get Action Scheduler retention period from execution settings
1775 * Converts days to seconds for Action Scheduler
1776 *
1777 * @param int $default_seconds Default retention period in seconds
1778 * @return int Configured retention period in seconds
1779 */
1780 public function get_action_scheduler_retention_period($default_seconds) {
1781 // Only apply if Action Scheduler is active
1782 if (!class_exists('ActionScheduler')) {
1783 return $default_seconds; // Default is 30 days = 2592000 seconds
1784 }
1785
1786 $cleanup_days = $this->get_execution_setting('queue_cleanup_days');
1787 return $cleanup_days * DAY_IN_SECONDS;
1788 }
1789
1790 /*
1791 * Sync setting on CRUD term category
1792 */
1793
1794 public function admin_crud_term($term_id,$term_tax_id,$taxonomy)
1795 {
1796 # Handle term creation, update, or deletion
1797 $this->sync_term($term_id, $taxonomy);
1798
1799 }
1800
1801
1802 /*
1803 * Sync setting on Delete term category
1804 */
1805
1806 public function admin_delete_term($term_id,$taxonomy)
1807 {
1808 # Handle term deletion
1809 $this->sync_term($term_id, $taxonomy);
1810 }
1811
1812 /*
1813 * Handle category deletion - Sync after category is deleted
1814 * This hook fires AFTER the category is fully deleted from the database
1815 */
1816 public function admin_delete_category($term_id)
1817 {
1818 try {
1819 # Initialize MetaSync API request class and trigger synchronization
1820 (new Metasync_Sync_Requests())->SyncCustomerParams();
1821 } catch (Exception $e) {
1822 # Log any API request errors for debugging
1823 error_log('Metasync API Error: ' . $e->getMessage());
1824 }
1825 }
1826
1827 /*
1828 * Call the SYNC API
1829 */
1830 private function sync_term($term_id, $taxonomy)
1831 {
1832 # Ensure the term belongs to the 'category' taxonomy and is not an error
1833 if ($taxonomy !== 'category' ) return;
1834
1835 try {
1836 # Initialize MetaSync API request class and trigger synchronization
1837 (new Metasync_Sync_Requests())->SyncCustomerParams();
1838 } catch (Exception $e) {
1839 # Log any API request errors for debugging
1840 error_log('Metasync API Error: ' . $e->getMessage());
1841 }
1842 }
1843
1844 /**
1845 * Dashboard page callback
1846 */
1847 public function create_admin_dashboard_page()
1848 {
1849 Metasync_Admin_Pages::get_instance($this)->create_admin_dashboard_page();
1850 }
1851
1852 /**
1853 * Robots.txt page callback
1854 */
1855 public function create_admin_robots_txt_page()
1856 {
1857 Metasync_Admin_Pages::get_instance($this)->create_admin_robots_txt_page();
1858 }
1859
1860 /**
1861 * Media Optimization page callback
1862 */
1863 public function create_admin_media_optimization_page()
1864 {
1865 ?>
1866 <div class="wrap metasync-dashboard-wrap" data-theme="<?php echo esc_attr(get_option('metasync_theme', 'dark')); ?>">
1867
1868 <?php $this->render_plugin_header('Media Optimization'); ?>
1869
1870 <?php $this->render_navigation_menu('media_optimization'); ?>
1871
1872 <?php
1873 // Load media optimization settings class
1874 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-settings.php';
1875
1876 $save_success = false;
1877
1878 // Handle form submissions
1879 if (isset($_POST['metasync_media_optimization_nonce'])) {
1880 check_admin_referer('metasync_save_media_optimization', 'metasync_media_optimization_nonce');
1881
1882 // Handle reset to defaults
1883 if (!empty($_POST['metasync_media_reset'])) {
1884 $defaults = Metasync_Media_Settings::get_defaults();
1885 Metasync_Media_Settings::save_settings($defaults);
1886 $save_success = true;
1887 } elseif (isset($_POST['metasync_media'])) {
1888 $input = wp_unslash($_POST['metasync_media']);
1889 Metasync_Media_Settings::save_settings($input);
1890 $save_success = true;
1891 }
1892 }
1893
1894 // Tab handling
1895 $current_tab = isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'settings';
1896
1897 // Prepare image library data
1898 $list_table = null;
1899 $stats = null;
1900 $batch_progress = null;
1901
1902 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-library-list-table.php';
1903 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-batch-optimizer.php';
1904
1905 $list_table = new Metasync_Media_Library_List_Table();
1906 $list_table->prepare_items();
1907
1908 $stats = Metasync_Media_Library_List_Table::get_stats();
1909 $batch_progress = Metasync_Media_Batch_Optimizer::get_progress();
1910
1911 // Render the admin page view
1912 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/views/admin-page.php';
1913 ?>
1914 </div>
1915 <?php
1916 }
1917
1918 // ── Media Optimization AJAX Handlers ──
1919
1920 /**
1921 * AJAX: Optimize a single image.
1922 */
1923 public function ajax_optimize_single_image()
1924 {
1925 check_ajax_referer('metasync_media_opt_nonce', 'nonce');
1926
1927 if (!current_user_can('upload_files')) {
1928 wp_send_json_error(__('Permission denied.', 'metasync'));
1929 }
1930
1931 $attachment_id = isset($_POST['attachment_id']) ? absint($_POST['attachment_id']) : 0;
1932 if (!$attachment_id) {
1933 wp_send_json_error(__('Invalid attachment ID.', 'metasync'));
1934 }
1935
1936 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-settings.php';
1937 $settings = Metasync_Media_Settings::get_settings();
1938
1939 $success = Metasync_Image_Converter::convert_attachment($attachment_id, $settings);
1940
1941 if ($success) {
1942 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-library-list-table.php';
1943 wp_send_json_success([
1944 'status_html' => Metasync_Media_Library_List_Table::render_status_html($attachment_id),
1945 ]);
1946 }
1947
1948 wp_send_json_error(__('Optimization failed.', 'metasync'));
1949 }
1950
1951 /**
1952 * AJAX: Revert a single image.
1953 */
1954 public function ajax_revert_single_image()
1955 {
1956 check_ajax_referer('metasync_media_opt_nonce', 'nonce');
1957
1958 if (!current_user_can('upload_files')) {
1959 wp_send_json_error(__('Permission denied.', 'metasync'));
1960 }
1961
1962 $attachment_id = isset($_POST['attachment_id']) ? absint($_POST['attachment_id']) : 0;
1963 if (!$attachment_id) {
1964 wp_send_json_error(__('Invalid attachment ID.', 'metasync'));
1965 }
1966
1967 $success = Metasync_Image_Converter::revert_attachment($attachment_id);
1968
1969 if ($success) {
1970 wp_send_json_success();
1971 }
1972
1973 wp_send_json_error(__('Revert failed. Original file may not exist (replace strategy).', 'metasync'));
1974 }
1975
1976 /**
1977 * AJAX: Start batch optimization.
1978 */
1979 public function ajax_start_batch_optimize()
1980 {
1981 check_ajax_referer('metasync_media_opt_nonce', 'nonce');
1982
1983 if (!current_user_can('manage_options')) {
1984 wp_send_json_error(__('Permission denied.', 'metasync'));
1985 }
1986
1987 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-settings.php';
1988 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-batch-optimizer.php';
1989
1990 $settings = Metasync_Media_Settings::get_settings();
1991 $progress = Metasync_Media_Batch_Optimizer::start_batch($settings);
1992
1993 wp_send_json_success($progress);
1994 }
1995
1996 /**
1997 * AJAX: Cancel batch optimization.
1998 */
1999 public function ajax_cancel_batch_optimize()
2000 {
2001 check_ajax_referer('metasync_media_opt_nonce', 'nonce');
2002
2003 if (!current_user_can('manage_options')) {
2004 wp_send_json_error(__('Permission denied.', 'metasync'));
2005 }
2006
2007 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-batch-optimizer.php';
2008 Metasync_Media_Batch_Optimizer::cancel_batch();
2009
2010 wp_send_json_success();
2011 }
2012
2013 /**
2014 * AJAX: Get batch progress + stats.
2015 */
2016 public function ajax_batch_progress()
2017 {
2018 check_ajax_referer('metasync_media_opt_nonce', 'nonce');
2019
2020 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-batch-optimizer.php';
2021 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-library-list-table.php';
2022
2023 $progress = Metasync_Media_Batch_Optimizer::get_progress();
2024 $progress['stats'] = Metasync_Media_Library_List_Table::get_stats();
2025
2026 wp_send_json_success($progress);
2027 }
2028
2029 /**
2030 * AJAX: Bulk optimize selected images.
2031 */
2032 public function ajax_bulk_optimize_selected()
2033 {
2034 check_ajax_referer('metasync_media_opt_nonce', 'nonce');
2035
2036 if (!current_user_can('upload_files')) {
2037 wp_send_json_error(__('Permission denied.', 'metasync'));
2038 }
2039
2040 $ids = isset($_POST['ids']) ? array_map('absint', explode(',', sanitize_text_field($_POST['ids']))) : [];
2041 $ids = array_filter($ids);
2042
2043 if (empty($ids)) {
2044 wp_send_json_error(__('No images selected.', 'metasync'));
2045 }
2046
2047 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-settings.php';
2048 $settings = Metasync_Media_Settings::get_settings();
2049
2050 $success = 0;
2051 $failed = 0;
2052
2053 foreach ($ids as $id) {
2054 if (Metasync_Image_Converter::convert_attachment($id, $settings)) {
2055 $success++;
2056 } else {
2057 $failed++;
2058 }
2059 }
2060
2061 wp_send_json_success([
2062 'success' => $success,
2063 'failed' => $failed,
2064 ]);
2065 }
2066
2067 /**
2068 * AJAX: Process one batch tick (browser-driven chaining).
2069 */
2070 public function ajax_process_batch_tick()
2071 {
2072 check_ajax_referer('metasync_media_opt_nonce', 'nonce');
2073
2074 if (!current_user_can('manage_options')) {
2075 wp_send_json_error(__('Permission denied.', 'metasync'));
2076 }
2077
2078 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-batch-optimizer.php';
2079 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-library-list-table.php';
2080
2081 $progress = Metasync_Media_Batch_Optimizer::process_ajax_tick();
2082 $progress['stats'] = Metasync_Media_Library_List_Table::get_stats();
2083
2084 wp_send_json_success($progress);
2085 }
2086
2087 /**
2088 * Cron handler: Process batch optimization tick.
2089 */
2090 public function handle_media_batch_cron()
2091 {
2092 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-settings.php';
2093 require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-batch-optimizer.php';
2094
2095 Metasync_Media_Batch_Optimizer::process_batch_tick();
2096 }
2097
2098 /**
2099 * Report Issue page callback
2100 */
2101 public function create_admin_report_issue_page()
2102 {
2103 // Load the Report Issue view
2104 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-report-issue.php';
2105 }
2106
2107 /**
2108 * XML Sitemap page callback
2109 */
2110 public function create_admin_xml_sitemap_page()
2111 {
2112 // Load sitemap generator class
2113 require_once plugin_dir_path(dirname(__FILE__)) . 'sitemap/class-metasync-sitemap-generator.php';
2114
2115 $sitemap_generator = new Metasync_Sitemap_Generator();
2116
2117 // Handle form submissions
2118 if (isset($_POST['metasync_sitemap_nonce'])) {
2119 check_admin_referer('metasync_sitemap_action', 'metasync_sitemap_nonce');
2120
2121 if (isset($_POST['generate_sitemap'])) {
2122 // Auto-disable other sitemap generators before generating
2123 $disabled_plugins = $sitemap_generator->disable_other_sitemap_generators();
2124
2125 $result = $sitemap_generator->generate_sitemap();
2126
2127 if (is_wp_error($result)) {
2128 echo '<div class="notice notice-error"><p>' . esc_html($result->get_error_message()) . '</p></div>';
2129 } else {
2130 $message = esc_html__('Sitemap generated successfully!', 'metasync');
2131 if ($disabled_plugins) {
2132 $message .= ' ' . esc_html__('Conflicting sitemap generators have been automatically disabled.', 'metasync');
2133 }
2134
2135 // Check if robots.txt was updated
2136 $robots_result = get_transient('metasync_sitemap_robots_updated');
2137 if ($robots_result && $robots_result['success']) {
2138 if ($robots_result['action'] === 'added') {
2139 $message .= ' ' . esc_html__('Sitemap URL has been added to robots.txt.', 'metasync');
2140 } elseif ($robots_result['action'] === 'updated') {
2141 $message .= ' ' . esc_html__('Sitemap URL has been updated in robots.txt.', 'metasync');
2142 } elseif ($robots_result['action'] === 'created') {
2143 $message .= ' ' . esc_html__('robots.txt file has been created with sitemap URL.', 'metasync');
2144 }
2145 delete_transient('metasync_sitemap_robots_updated');
2146 }
2147
2148 echo '<div class="notice notice-success"><p>' . esc_html($message) . '</p></div>';
2149 }
2150 } elseif (isset($_POST['enable_auto_update'])) {
2151 update_option('metasync_sitemap_auto_update', true);
2152 $sitemap_generator->setup_auto_update_hooks();
2153 echo '<div class="notice notice-success"><p>' . esc_html__('Auto-update enabled!', 'metasync') . '</p></div>';
2154 } elseif (isset($_POST['disable_auto_update'])) {
2155 update_option('metasync_sitemap_auto_update', false);
2156 echo '<div class="notice notice-success"><p>' . esc_html__('Auto-update disabled!', 'metasync') . '</p></div>';
2157 } elseif (isset($_POST['delete_sitemap'])) {
2158 // Delete the sitemap file (physical and/or virtual)
2159 $deleted = $sitemap_generator->delete_sitemap();
2160 if ($deleted) {
2161 // Also disable auto-update when deleting
2162 update_option('metasync_sitemap_auto_update', false);
2163 echo '<div class="notice notice-success"><p>' . esc_html__('Sitemap deleted successfully!', 'metasync') . '</p></div>';
2164 } else {
2165 echo '<div class="notice notice-error"><p>' . esc_html__('Failed to delete sitemap. The file may not exist or is not writable.', 'metasync') . '</p></div>';
2166 }
2167 } elseif (isset($_POST['enable_other_sitemaps'])) {
2168 // Re-enable other sitemap plugins
2169 $enabled_plugins = $sitemap_generator->enable_other_sitemap_generators();
2170 if ($enabled_plugins) {
2171 echo '<div class="notice notice-success"><p>' . esc_html__('Other sitemap plugins have been re-enabled successfully!', 'metasync') . '</p></div>';
2172 } else {
2173 echo '<div class="notice notice-info"><p>' . esc_html__('No sitemap plugins were found to re-enable.', 'metasync') . '</p></div>';
2174 }
2175 }
2176 }
2177
2178 // Get sitemap info
2179 $sitemap_exists = $sitemap_generator->sitemap_exists();
2180 $sitemap_url = $sitemap_generator->get_sitemap_url();
2181 $url_count = $sitemap_generator->count_urls();
2182 $last_generated = $sitemap_generator->get_last_generated_time();
2183 $auto_update_enabled = get_option('metasync_sitemap_auto_update', false);
2184 $active_sitemap_plugins = $sitemap_generator->check_active_sitemap_plugins();
2185
2186 // Load view
2187 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-xml-sitemap.php';
2188 }
2189
2190 /**
2191 * Custom Pages page callback
2192 */
2193 public function create_admin_custom_pages_page()
2194 {
2195 Metasync_Admin_Pages::get_instance($this)->create_admin_custom_pages_page();
2196 }
2197
2198 /**
2199 * 404 Monitor page callback
2200 */
2201 public function create_admin_404_monitor_page()
2202 {
2203 Metasync_Admin_Pages::get_instance($this)->create_admin_404_monitor_page();
2204 }
2205
2206 /**
2207 * Site Verification page callback
2208 */
2209 public function create_admin_search_engine_verification_page()
2210 {
2211 Metasync_Admin_Pages::get_instance($this)->create_admin_search_engine_verification_page();
2212 }
2213
2214 /**
2215 * Local Business page callback
2216 */
2217 public function create_admin_local_business_page()
2218 {
2219 Metasync_Admin_Pages::get_instance($this)->create_admin_local_business_page();
2220 }
2221
2222 /**
2223 * Code Snippets page callback
2224 */
2225 public function create_admin_code_snippets_page()
2226 {
2227 Metasync_Admin_Pages::get_instance($this)->create_admin_code_snippets_page();
2228 }
2229
2230 /**
2231 * Google Instant Index Setting page callback
2232 */
2233 public function create_admin_google_instant_index_page()
2234 {
2235 ?>
2236 <div class="wrap metasync-dashboard-wrap" data-theme="<?php echo esc_attr(get_option('metasync_theme', 'dark')); ?>">
2237
2238 <?php $this->render_plugin_header('Instant Indexing'); ?>
2239
2240 <?php $this->render_navigation_menu('instant_index'); ?>
2241
2242 <?php
2243 // Set up variables for the view template
2244 $options = get_option('metasync_options_instant_indexing', ['json_key' => '', 'post_types' => []]);
2245 $json_key = isset($options['json_key']) ? $options['json_key'] : '';
2246 $google_guide_url = 'https://developers.google.com/search/apis/indexing-api/v3/quickstart';
2247 $post_types_settings = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : [];
2248 include_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-instant-settings.php';
2249 ?>
2250 </div>
2251 <?php
2252 }
2253
2254 /**
2255 * Google Console page callback
2256 */
2257 public function create_admin_google_console_page()
2258 {
2259 ?>
2260 <div class="wrap metasync-dashboard-wrap" data-theme="<?php echo esc_attr(get_option('metasync_theme', 'dark')); ?>">
2261
2262 <?php $this->render_plugin_header('Google Console'); ?>
2263
2264 <?php $this->render_navigation_menu('google_console'); ?>
2265
2266 <?php
2267 // Check if Google service account is configured via google_index_direct()
2268 $service_info = function_exists('google_index_direct') ? google_index_direct()->get_service_account_info() : ['error' => 'Module not loaded'];
2269 $is_configured = !isset($service_info['error']);
2270 include_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-console.php';
2271 ?>
2272 </div>
2273 <?php
2274 }
2275
2276 /**
2277 * Bing Console page callback
2278 */
2279 public function create_admin_bing_console_page()
2280 {
2281 ?>
2282 <div class="wrap metasync-dashboard-wrap" data-theme="<?php echo esc_attr(get_option('metasync_theme', 'dark')); ?>">
2283
2284 <?php $this->render_plugin_header('Bing Console'); ?>
2285
2286 <?php $this->render_navigation_menu('bing_console'); ?>
2287
2288 <?php
2289 require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php';
2290 $bing_instant_index = new Metasync_Bing_Instant_Index();
2291 $bing_instant_index->show_bing_instant_indexing_console();
2292 ?>
2293 </div>
2294 <?php
2295 }
2296
2297 /**
2298 * General Options page callback
2299 */
2300 public function create_admin_optimal_settings_page()
2301 {
2302 Metasync_Admin_Pages::get_instance($this)->create_admin_optimal_settings_page();
2303 }
2304
2305 /**
2306 * Global Options page callback
2307 */
2308 public function create_admin_global_settings_page()
2309 {
2310 Metasync_Admin_Pages::get_instance($this)->create_admin_global_settings_page();
2311 }
2312
2313 /**
2314 * Common Meta Options page callback
2315 */
2316 public function create_admin_common_meta_settings_page()
2317 {
2318 Metasync_Admin_Pages::get_instance($this)->create_admin_common_meta_settings_page();
2319 }
2320
2321 /**
2322 * Social meta page callback
2323 */
2324 public function create_admin_social_meta_page()
2325 {
2326 Metasync_Admin_Pages::get_instance($this)->create_admin_social_meta_page();
2327 }
2328
2329
2330 /**
2331 * Indexation Control page callback
2332 */
2333 public function create_admin_seo_controls_page()
2334 {
2335 Metasync_Admin_Pages::get_instance($this)->create_admin_seo_controls_page();
2336 }
2337
2338 /**
2339 * Site Optimal Settings page callback
2340 */
2341 public function optimization_settings_options()
2342 {
2343 Metasync_Admin_Pages::get_instance($this)->optimization_settings_options();
2344 }
2345
2346 /**
2347 * redirection page callback with tabs
2348 */
2349 public function create_admin_redirections_page()
2350 {
2351 Metasync_Redirections_Admin::get_instance($this->db_redirection, $this)->create_admin_redirections_page();
2352 }
2353
2354 /**
2355 * Display transient error/success messages for redirections
2356 */
2357 public function display_redirection_messages()
2358 {
2359 Metasync_Redirections_Admin::get_instance($this->db_redirection, $this)->display_redirection_messages();
2360 }
2361
2362 /**
2363 * AJAX handler for updating database structure
2364 */
2365 public function ajax_update_db_structure()
2366 {
2367 Metasync_Admin_Ajax::instance()->ajax_update_db_structure();
2368 }
2369
2370 /**
2371 * AJAX handler to save wizard progress
2372 *
2373 * @since 1.0.0
2374 */
2375 public function ajax_save_wizard_progress()
2376 {
2377 Metasync_Admin_Ajax::instance()->ajax_save_wizard_progress();
2378 }
2379
2380 /**
2381 * AJAX handler to complete wizard
2382 *
2383 * @since 1.0.0
2384 */
2385 public function ajax_complete_wizard()
2386 {
2387 Metasync_Admin_Ajax::instance()->ajax_complete_wizard();
2388 }
2389
2390 /**
2391 * AJAX handler to validate robots.txt content
2392 */
2393 public function ajax_validate_robots()
2394 {
2395 Metasync_Admin_Ajax::instance()->ajax_validate_robots();
2396 }
2397
2398 /**
2399 * AJAX handler to get default robots.txt content
2400 */
2401 public function ajax_get_default_robots()
2402 {
2403 Metasync_Admin_Ajax::instance()->ajax_get_default_robots();
2404 }
2405
2406 /**
2407 * AJAX handler to preview robots.txt backup content
2408 */
2409 public function ajax_preview_robots_backup()
2410 {
2411 Metasync_Admin_Ajax::instance()->ajax_preview_robots_backup();
2412 }
2413
2414 /**
2415 * AJAX handler to delete robots.txt backup
2416 */
2417 public function ajax_delete_robots_backup()
2418 {
2419 Metasync_Admin_Ajax::instance()->ajax_delete_robots_backup();
2420 }
2421
2422 /**
2423 * AJAX handler to restore robots.txt backup
2424 */
2425 public function ajax_restore_robots_backup()
2426 {
2427 Metasync_Admin_Ajax::instance()->ajax_restore_robots_backup();
2428 }
2429 public function ajax_create_redirect_from_404()
2430 {
2431 Metasync_Admin_Ajax::instance()->ajax_create_redirect_from_404();
2432 }
2433
2434 /**
2435 * AJAX handler for testing host blocking with GET request
2436 */
2437 public function ajax_test_host_blocking_get()
2438 {
2439 Metasync_Admin_Ajax::instance()->ajax_test_host_blocking_get();
2440 }
2441
2442 /**
2443 * AJAX handler for testing host blocking with POST request
2444 */
2445 public function ajax_test_host_blocking_post()
2446 {
2447 Metasync_Admin_Ajax::instance()->ajax_test_host_blocking_post();
2448 }
2449
2450 /**
2451 * Register REST API endpoint for ping
2452 */
2453 public function register_ping_rest_endpoint()
2454 {
2455 register_rest_route('metasync/v1', '/ping', array(
2456 'methods' => array('GET', 'POST'),
2457 'callback' => array($this, 'handle_ping_rest_endpoint'),
2458 'permission_callback' => '__return_true', // Allow public access
2459 'args' => array(
2460 'test' => array(
2461 'description' => 'Optional test parameter',
2462 'type' => 'string',
2463 'sanitize_callback' => 'sanitize_text_field',
2464 ),
2465 ),
2466 ));
2467 }
2468
2469 /**
2470 * Handle REST API ping endpoint
2471 */
2472 public function handle_ping_rest_endpoint($request)
2473 {
2474 // Get request method
2475 $method = $request->get_method();
2476
2477 // Prepare response data
2478 $response_data = array(
2479 'response' => 'pong',
2480 'method' => $method,
2481 'timestamp' => current_time('mysql'),
2482 'site_url' => home_url(),
2483 'plugin_version' => METASYNC_VERSION
2484 );
2485
2486 // Add request data for POST requests
2487 if ($method === 'POST') {
2488 $body = $request->get_body();
2489 if (!empty($body)) {
2490 $response_data['received_data'] = json_decode($body, true);
2491 }
2492
2493 // Add any query parameters
2494 $params = $request->get_params();
2495 if (!empty($params)) {
2496 $response_data['query_params'] = $params;
2497 }
2498 }
2499
2500 // Add test parameter if provided
2501 $test_param = $request->get_param('test');
2502 if (!empty($test_param)) {
2503 $response_data['test_param'] = $test_param;
2504 }
2505
2506 return new WP_REST_Response($response_data, 200);
2507 }
2508
2509 /**
2510 * Site error logs page callback
2511 */
2512
2513 public function create_admin_error_logs_page()
2514 {
2515 Metasync_Admin_Pages::get_instance($this)->create_admin_error_logs_page();
2516 }
2517
2518 /**
2519 * Compatibility page callback
2520 */
2521 public function create_admin_compatibility_page()
2522 {
2523 Metasync_Compatibility_Checker::instance()->create_admin_compatibility_page($this);
2524 }
2525
2526 /**
2527 * Sync Log page callback
2528 */
2529 public function create_admin_sync_log_page()
2530 {
2531 Metasync_Admin_Pages::get_instance($this)->create_admin_sync_log_page();
2532 }
2533 /**
2534 * Build filter query string for pagination
2535 */
2536 private function build_filter_query_string($filters)
2537 {
2538 $query_parts = [];
2539 foreach ($filters as $key => $value) {
2540 if (!empty($value)) {
2541 $query_parts[] = $key . '=' . urlencode($value);
2542 }
2543 }
2544 return !empty($query_parts) ? '&' . implode('&', $query_parts) : '';
2545 }
2546
2547 /**
2548 * Handle AJAX requests for sync log data
2549 */
2550 private function handle_sync_log_ajax()
2551 {
2552 // This can be used for future AJAX functionality like real-time updates
2553 wp_die();
2554 }
2555
2556 /**
2557 * Render compatibility sections
2558 */
2559 private function render_compatibility_sections()
2560 {
2561 Metasync_Compatibility_Checker::instance()->render_compatibility_sections();
2562 }
2563
2564 /**
2565 * Render Page Builders section
2566 */
2567 private function render_page_builders_section()
2568 {
2569 Metasync_Compatibility_Checker::instance()->render_page_builders_section();
2570 }
2571
2572 /**
2573 * Render SEO Plugins section
2574 */
2575 private function render_seo_plugins_section()
2576 {
2577 Metasync_Compatibility_Checker::instance()->render_seo_plugins_section();
2578 }
2579
2580 /**
2581 * Render Cache Plugins section
2582 */
2583 private function render_cache_plugins_section()
2584 {
2585 Metasync_Compatibility_Checker::instance()->render_cache_plugins_section();
2586 }
2587
2588 /**
2589 * Render Lock Section button for protected tabs
2590 *
2591 * @param string $tab The tab identifier (general, whitelabel, advanced)
2592 */
2593 private function render_lock_button($tab)
2594 {
2595 Metasync_Compatibility_Checker::instance()->render_lock_button($tab);
2596 }
2597
2598 /**
2599 * Get Page Builders compatibility information
2600 */
2601 private function get_page_builders_compatibility()
2602 {
2603 return Metasync_Compatibility_Checker::instance()->get_page_builders_compatibility();
2604 }
2605
2606 /**
2607 * Get SEO Plugins compatibility information
2608 */
2609 private function get_seo_plugins_compatibility()
2610 {
2611 return Metasync_Compatibility_Checker::instance()->get_seo_plugins_compatibility();
2612 }
2613
2614 /**
2615 * Get Cache Plugins compatibility information
2616 */
2617 private function get_cache_plugins_compatibility()
2618 {
2619 return Metasync_Compatibility_Checker::instance()->get_cache_plugins_compatibility();
2620 }
2621
2622 /**
2623 * Check if a plugin is installed and active
2624 * @deprecated Use get_plugin_status() instead
2625 */
2626 private function is_plugin_installed($plugin_file)
2627 {
2628 return Metasync_Compatibility_Checker::instance()->is_plugin_installed($plugin_file);
2629 }
2630
2631 /**
2632 * Get detailed plugin status (installed and/or active)
2633 * Checks multiple plugin file paths (e.g., free and premium versions)
2634 *
2635 * @param array $plugin_files Array of plugin file paths to check (e.g., ['free/plugin.php', 'pro/plugin.php'])
2636 * @param bool $is_core Whether this is a WordPress core feature (always installed/active)
2637 * @param string $theme_name Optional theme name to check if it's a theme instead of plugin
2638 * @return array ['is_installed' => bool, 'is_active' => bool, 'active_version' => string|null]
2639 */
2640 private function get_plugin_status($plugin_files, $is_core = false, $theme_name = null)
2641 {
2642 return Metasync_Compatibility_Checker::instance()->get_plugin_status($plugin_files, $is_core, $theme_name);
2643 }
2644
2645
2646 /**
2647 * Get plugin logo URL (optimized for performance)
2648 */
2649 private function get_plugin_logo($plugin_key, $type)
2650 {
2651 return Metasync_Compatibility_Checker::instance()->get_plugin_logo($plugin_key, $type);
2652 }
2653
2654
2655 public function creat_error_Logs_List()
2656 {
2657 Metasync_Admin_Pages::get_instance($this)->creat_error_Logs_List();
2658 }
2659
2660 /**
2661 * Site error logs page callback
2662 */
2663 public function create_admin_heartbeat_error_logs_page()
2664 {
2665 Metasync_Admin_Pages::get_instance($this)->create_admin_heartbeat_error_logs_page();
2666 }
2667
2668
2669 /**
2670 * Handle session management early for whitelabel functionality
2671 */
2672 private function handle_session_management_early()
2673 {
2674 Metasync_Connect_Manager::instance()->handle_session_management_early();
2675 }
2676
2677 /**
2678 * @deprecated 2.5.12 Use Metasync_Auth_Manager instead of sessions for authentication
2679 */
2680 private function safe_session_start() {
2681 // This method is deprecated and no longer used
2682 // Authentication now uses Metasync_Auth_Manager with WordPress transients and user meta
2683 _deprecated_function(__METHOD__, '2.5.12', 'Metasync_Auth_Manager');
2684 return Metasync_Session_Helper::safe_start();
2685 }
2686
2687 private function handle_whitelabel_session_logic()
2688 {
2689 Metasync_Connect_Manager::instance()->handle_whitelabel_session_logic();
2690 }
2691
2692 private function handle_whitelabel_password_early()
2693 {
2694 Metasync_Connect_Manager::instance()->handle_whitelabel_password_early();
2695 }
2696
2697 /**
2698 * Get accordion sections configuration for General Settings
2699 *
2700 * @return array Accordion sections with field IDs, icons, and descriptions
2701 */
2702 private function get_accordion_sections_config() {
2703 return Metasync_Settings_Fields::instance()->get_accordion_sections_config();
2704 }
2705
2706 /**
2707 * Get accordion sections configuration for Advanced Settings Tab
2708 *
2709 * @return array Accordion sections configuration
2710 */
2711 private function get_advanced_accordion_config() {
2712 return Metasync_Settings_Fields::instance()->get_advanced_accordion_config();
2713 }
2714
2715 /**
2716 * Render accordion sections for Advanced Settings Tab
2717 */
2718 public function render_advanced_accordion() {
2719 Metasync_Settings_Fields::instance()->render_advanced_accordion();
2720 }
2721
2722 /**
2723 * Render reset settings section for Advanced tab accordion
2724 */
2725 private function render_reset_settings_section() {
2726 Metasync_Settings_Fields::instance()->render_reset_settings_section();
2727 }
2728
2729 /**
2730 * Render Google Index API section for Indexation Control page
2731 */
2732 public function render_google_index_section() {
2733 Metasync_Settings_Fields::instance()->render_google_index_section();
2734 }
2735
2736 /**
2737 * Render Bing Index (IndexNow) section
2738 *
2739 * @since 2.6.0
2740 * @return void
2741 */
2742 public function render_bing_index_section() {
2743 Metasync_Settings_Fields::instance()->render_bing_index_section();
2744 }
2745
2746 /**
2747 * Render Plugin Access Roles section for Advanced tab accordion
2748 */
2749 private function render_plugin_access_roles_section() {
2750 Metasync_Settings_Fields::instance()->render_plugin_access_roles_section();
2751 }
2752
2753 /**
2754 * Check if the current user has access to the plugin based on role settings
2755 * Wrapper method that delegates to the common Metasync::current_user_has_plugin_access()
2756 * but also requires manage_options capability for admin area access
2757 *
2758 * @return bool True if user has access, false otherwise
2759 */
2760 public function current_user_has_plugin_access() {
2761 return Metasync::current_user_has_plugin_access();
2762 }
2763
2764 /**
2765 * Get default execution settings
2766 *
2767 * @return array Default execution settings
2768 */
2769 private function get_default_execution_settings() {
2770 return Metasync_Settings_Fields::instance()->get_default_execution_settings();
2771 }
2772
2773 /**
2774 * Get execution setting value
2775 *
2776 * @param string $key Setting key
2777 * @param mixed $default Default value if setting doesn't exist
2778 * @return mixed Setting value or default
2779 */
2780 public function get_execution_setting($key, $default = null) {
2781 return Metasync_Settings_Fields::instance()->get_execution_setting($key, $default);
2782 }
2783
2784 /**
2785 * Get all execution settings
2786 *
2787 * @return array All execution settings with defaults merged
2788 */
2789 public function get_all_execution_settings() {
2790 return Metasync_Settings_Fields::instance()->get_all_execution_settings();
2791 }
2792
2793 /**
2794 * Check if server allows changing memory limit
2795 * Tests if ini_set('memory_limit') is allowed
2796 *
2797 * @return bool True if memory limit can be changed, false otherwise
2798 */
2799 private function can_change_memory_limit() {
2800 return Metasync_Settings_Fields::instance()->can_change_memory_limit();
2801 }
2802
2803 /**
2804 * Get PHP server limits for display
2805 *
2806 * @return array Server limits (execution_time, memory_limit, can_change_memory)
2807 */
2808 private function get_server_limits() {
2809 return Metasync_Settings_Fields::instance()->get_server_limits();
2810 }
2811
2812 /**
2813 * Apply memory limit from execution settings
2814 * Only applies if server allows changing memory limit
2815 *
2816 * @return bool True if memory limit was applied, false otherwise
2817 */
2818 public function apply_memory_limit() {
2819 return Metasync_Settings_Fields::instance()->apply_memory_limit();
2820 }
2821
2822 /**
2823 * Parse memory limit string to MB
2824 *
2825 * @param string $memory_limit Memory limit string (e.g., "256M", "1G")
2826 * @return int Memory limit in MB
2827 */
2828 private function parse_memory_limit_to_mb($memory_limit) {
2829 return Metasync_Settings_Fields::instance()->parse_memory_limit_to_mb($memory_limit);
2830 }
2831
2832 /**
2833 * Render Execution Settings section for Advanced tab accordion
2834 */
2835 private function render_execution_settings_section() {
2836 Metasync_Settings_Fields::instance()->render_execution_settings_section();
2837 }
2838
2839 /**
2840 * Get tooltip content for settings fields
2841 *
2842 * @return array Field ID => Tooltip text mapping
2843 */
2844 private function get_field_tooltips() {
2845 return Metasync_Settings_Fields::instance()->get_field_tooltips();
2846 }
2847
2848 /**
2849 * Get the section key for a given field ID
2850 *
2851 * @param string $field_id The settings field ID
2852 * @return string|null Section key or null if not found
2853 */
2854 private function get_field_section($field_id) {
2855 return Metasync_Settings_Fields::instance()->get_field_section($field_id);
2856 }
2857
2858 /**
2859 * Render accordion sections for General Settings
2860 *
2861 * @param string $page The settings page slug
2862 */
2863 public function render_accordion_sections($page) {
2864 Metasync_Settings_Fields::instance()->render_accordion_sections($page);
2865 }
2866
2867 /**
2868 * Register and add settings
2869 */
2870 public function settings_page_init()
2871 {
2872 Metasync_Settings_Registration::instance()->settings_page_init();
2873 }
2874
2875 /**
2876 * Sanitize each setting field as needed
2877 *
2878 * @param array $input Contains all settings fields as array keys
2879 */
2880 public function sanitize($input)
2881 {
2882 return Metasync_Settings_Registration::instance()->sanitize($input);
2883 }
2884
2885 public function metasync_settings_genkey_callback()
2886 {
2887 Metasync_Settings_Fields::instance()->metasync_settings_genkey_callback();
2888 }
2889
2890 /**
2891 * Get the settings option array and print one of its values
2892 */
2893 public function linkgraph_token_callback()
2894 {
2895 Metasync_Settings_Fields::instance()->linkgraph_token_callback();
2896 }
2897
2898
2899 private function time_elapsed_string($datetime, $full = false)
2900 {
2901 return Metasync_Settings_Fields::instance()->time_elapsed_string($datetime, $full);
2902 }
2903
2904 /**
2905 * Get the settings option array and print one of its values
2906 */
2907 public function searchatlas_api_key_callback()
2908 {
2909 Metasync_Settings_Fields::instance()->searchatlas_api_key_callback();
2910 }
2911
2912
2913 /**
2914 * Site Verification Tools
2915 *
2916 * Bing Site Verification
2917 * Baidu Site Verification
2918 * Alexa Site Verification
2919 * Yandex Site Verification
2920 * Google Site Verification
2921 * Pinterest Site Verification
2922 * Norton Safe Web Site Verification
2923 */
2924
2925 /**
2926 * Get the settings option array and print one of its values
2927 */
2928 public function bing_site_verification_callback()
2929 {
2930 Metasync_Settings_Fields::instance()->bing_site_verification_callback();
2931 }
2932
2933
2934
2935
2936
2937 /**
2938 * Get the settings option array and print one of its values
2939 */
2940 public function yandex_site_verification_callback()
2941 {
2942 Metasync_Settings_Fields::instance()->yandex_site_verification_callback();
2943 }
2944
2945 /**
2946 * Get the settings option array and print one of its values
2947 */
2948 public function google_site_verification_callback()
2949 {
2950 Metasync_Settings_Fields::instance()->google_site_verification_callback();
2951 }
2952
2953 /**
2954 * Get the settings option array and print one of its values
2955 */
2956 public function pinterest_site_verification_callback()
2957 {
2958 Metasync_Settings_Fields::instance()->pinterest_site_verification_callback();
2959 }
2960
2961
2962
2963 /**
2964 * Local SEO for business and person
2965 *
2966 */
2967
2968 /**
2969 * Get the settings option array and print one of its values
2970 */
2971 public function local_seo_person_organization_callback()
2972 {
2973 Metasync_Settings_Fields::instance()->local_seo_person_organization_callback();
2974 }
2975
2976 /**
2977 * Get the settings option array and print one of its values
2978 */
2979 public function local_seo_name_callback()
2980 {
2981 Metasync_Settings_Fields::instance()->local_seo_name_callback();
2982 }
2983
2984 /**
2985 * Get the settings option array and print one of its values
2986 */
2987 public function local_seo_logo_callback()
2988 {
2989 Metasync_Settings_Fields::instance()->local_seo_logo_callback();
2990 }
2991
2992 /**
2993 * Get the settings option array and print one of its values
2994 */
2995 public function local_seo_url_callback()
2996 {
2997 Metasync_Settings_Fields::instance()->local_seo_url_callback();
2998 }
2999
3000 /**
3001 * Get the settings option array and print one of its values
3002 */
3003 public function local_seo_email_callback()
3004 {
3005 Metasync_Settings_Fields::instance()->local_seo_email_callback();
3006 }
3007
3008 /**
3009 * Get the settings option array and print one of its values
3010 */
3011 public function local_seo_phone_callback()
3012 {
3013 Metasync_Settings_Fields::instance()->local_seo_phone_callback();
3014 }
3015
3016 /**
3017 * Get the settings option array and print one of its values
3018 */
3019 public function local_seo_address_callback()
3020 {
3021 Metasync_Settings_Fields::instance()->local_seo_address_callback();
3022 }
3023
3024 /**
3025 * Get the settings option array and print one of its values
3026 */
3027 public function local_seo_business_type_callback()
3028 {
3029 Metasync_Settings_Fields::instance()->local_seo_business_type_callback();
3030 }
3031
3032
3033
3034 /**
3035 * Get the settings option array and print one of its values
3036 */
3037 public function local_seo_opening_hours_callback()
3038 {
3039 Metasync_Settings_Fields::instance()->local_seo_opening_hours_callback();
3040 }
3041
3042 /**
3043 * Get the settings option array and print one of its values
3044 */
3045 public function local_seo_phone_numbers_callback()
3046 {
3047 Metasync_Settings_Fields::instance()->local_seo_phone_numbers_callback();
3048 }
3049
3050 /**
3051 * Get the settings option array and print one of its values
3052 */
3053 public function local_seo_price_range_callback()
3054 {
3055 Metasync_Settings_Fields::instance()->local_seo_price_range_callback();
3056 }
3057
3058 /**
3059 * Get the settings option array and print one of its values
3060 */
3061 public function local_seo_about_page_callback()
3062 {
3063 Metasync_Settings_Fields::instance()->local_seo_about_page_callback();
3064 }
3065
3066 /**
3067 * Get the settings option array and print one of its values
3068 */
3069 public function local_seo_contact_page_callback()
3070 {
3071 Metasync_Settings_Fields::instance()->local_seo_contact_page_callback();
3072 }
3073
3074 /**
3075 * Get the settings option array and print one of its values
3076 */
3077 public function local_seo_map_key_callback()
3078 {
3079 Metasync_Settings_Fields::instance()->local_seo_map_key_callback();
3080 }
3081
3082 /**
3083 * Get the settings option array and print one of its values
3084 */
3085 public function local_seo_geo_coordinates_callback()
3086 {
3087 Metasync_Settings_Fields::instance()->local_seo_geo_coordinates_callback();
3088 }
3089
3090 /**
3091 * Get the settings option array and print one of its values
3092 */
3093 public function header_snippets_callback()
3094 {
3095 Metasync_Settings_Fields::instance()->header_snippets_callback();
3096 }
3097
3098 /**
3099 * Get the settings option array and print one of its values
3100 */
3101 public function footer_snippets_callback()
3102 {
3103 Metasync_Settings_Fields::instance()->footer_snippets_callback();
3104 }
3105
3106 /**
3107 * Get the settings option array and print one of its values
3108 */
3109 public function no_index_posts_callback()
3110 {
3111 Metasync_Settings_Fields::instance()->no_index_posts_callback();
3112 }
3113
3114 /**
3115 * Get the settings option array and print one of its values
3116 */
3117 public function no_follow_links_callback()
3118 {
3119 Metasync_Settings_Fields::instance()->no_follow_links_callback();
3120 }
3121
3122 /**
3123 * Get the settings option array and print one of its values
3124 */
3125 public function open_external_links_callback()
3126 {
3127 Metasync_Settings_Fields::instance()->open_external_links_callback();
3128 }
3129
3130 /**
3131 * Get the settings option array and print one of its values
3132 */
3133 public function add_alt_image_tags_callback()
3134 {
3135 Metasync_Settings_Fields::instance()->add_alt_image_tags_callback();
3136 }
3137
3138 /**
3139 * Get the settings option array and print one of its values
3140 */
3141 public function add_title_image_tags_callback()
3142 {
3143 Metasync_Settings_Fields::instance()->add_title_image_tags_callback();
3144 }
3145
3146 /**
3147 * Get the settings option array and print one of its values
3148 */
3149 public function site_type_callback()
3150 {
3151 Metasync_Settings_Fields::instance()->site_type_callback();
3152 }
3153
3154 /**
3155 * Get the settings option array and print one of its values
3156 */
3157 public function site_business_type_callback()
3158 {
3159 Metasync_Settings_Fields::instance()->site_business_type_callback();
3160 }
3161
3162 /**
3163 * Get the settings option array and print one of its values
3164 */
3165 public function site_company_name_callback()
3166 {
3167 Metasync_Settings_Fields::instance()->site_company_name_callback();
3168 }
3169
3170 /**
3171 * Get the settings option array and print one of its values
3172 */
3173 public function site_google_logo_callback()
3174 {
3175 Metasync_Settings_Fields::instance()->site_google_logo_callback();
3176 }
3177
3178 /**
3179 * Get the settings option array and print one of its values
3180 */
3181 public function site_social_share_image_callback()
3182 {
3183 Metasync_Settings_Fields::instance()->site_social_share_image_callback();
3184 }
3185
3186 /**
3187 * Get the settings option array and print one of its values
3188 */
3189 public function common_robot_meta_tags_callback()
3190 {
3191 Metasync_Settings_Fields::instance()->common_robot_meta_tags_callback();
3192 }
3193
3194 /**
3195 * Backward compatibility alias for common_robot_mata_tags_callback
3196 * @deprecated Use common_robot_meta_tags_callback() instead
3197 */
3198 public function common_robot_mata_tags_callback()
3199 {
3200 Metasync_Settings_Fields::instance()->common_robot_mata_tags_callback();
3201 }
3202
3203 /**
3204 * Get the settings option array and print one of its values
3205 */
3206 public function advance_robot_meta_tags_callback()
3207 {
3208 Metasync_Settings_Fields::instance()->advance_robot_meta_tags_callback();
3209 }
3210
3211 /**
3212 * Backward compatibility alias for advance_robot_mata_tags_callback
3213 * @deprecated Use advance_robot_meta_tags_callback() instead
3214 */
3215 public function advance_robot_mata_tags_callback()
3216 {
3217 Metasync_Settings_Fields::instance()->advance_robot_mata_tags_callback();
3218 }
3219
3220 /**
3221 * Get the settings option array and print one of its values
3222 */
3223 public function global_twitter_card_type_callback()
3224 {
3225 Metasync_Settings_Fields::instance()->global_twitter_card_type_callback();
3226 }
3227
3228 /**
3229 * Get the settings option array and print one of its values
3230 */
3231 public function global_open_graph_meta_callback()
3232 {
3233 Metasync_Settings_Fields::instance()->global_open_graph_meta_callback();
3234 }
3235
3236 /**
3237 * Get the settings option array and print one of its values
3238 */
3239 public function global_facebook_meta_callback()
3240 {
3241 Metasync_Settings_Fields::instance()->global_facebook_meta_callback();
3242 }
3243
3244 /**
3245 * Get the settings option array and print one of its values
3246 */
3247 public function global_twitter_meta_callback()
3248 {
3249 Metasync_Settings_Fields::instance()->global_twitter_meta_callback();
3250 }
3251
3252 /**
3253 * Get the settings option array and print one of its values
3254 */
3255 public function facebook_page_url_callback()
3256 {
3257 Metasync_Settings_Fields::instance()->facebook_page_url_callback();
3258 }
3259
3260 /**
3261 * Get the settings option array and print one of its values
3262 */
3263 public function facebook_authorship_callback()
3264 {
3265 Metasync_Settings_Fields::instance()->facebook_authorship_callback();
3266 }
3267
3268 /**
3269 * Get the settings option array and print one of its values
3270 */
3271 public function facebook_admin_callback()
3272 {
3273 Metasync_Settings_Fields::instance()->facebook_admin_callback();
3274 }
3275
3276 /**
3277 * Get the settings option array and print one of its values
3278 */
3279 public function facebook_app_callback()
3280 {
3281 Metasync_Settings_Fields::instance()->facebook_app_callback();
3282 }
3283
3284 /**
3285 * Get the settings option array and print one of its values
3286 */
3287 public function facebook_secret_callback()
3288 {
3289 Metasync_Settings_Fields::instance()->facebook_secret_callback();
3290 }
3291
3292 /**
3293 * Get the settings option array and print one of its values
3294 */
3295 public function twitter_username_callback()
3296 {
3297 Metasync_Settings_Fields::instance()->twitter_username_callback();
3298 }
3299
3300 /**
3301 * Get business types as choices in local business.
3302 *
3303 * @return array
3304 */
3305 public static function get_business_types()
3306 {
3307 return Metasync_Settings_Fields::get_business_types();
3308 }
3309
3310 /**
3311 * Display a dashboard warning when using the plain permalink structure.
3312 * @param $data An array of data passed.
3313 */
3314 public function permalink_structure_dashboard_warning() {
3315 $current_permalink_structure = get_option('permalink_structure');
3316
3317 # Get the plugin name using centralized method
3318 $plugin_name = Metasync::get_effective_plugin_name();
3319 $current_rewrite_rules = get_option('rewrite_rules');
3320 # Check if the current permalink structure is set to "Plain"
3321 if (($current_permalink_structure == '/%post_id%/' || $current_permalink_structure == '') && $current_rewrite_rules == '') {
3322
3323 # Show admin notice with plugin name included in the message
3324 printf(
3325 '<div class="notice notice-error is-dismissible">
3326 <p>
3327 <b>Warning from %s</b><br>
3328 To ensure compatibility, please update your permalink structure to any option other than "Plain".
3329 For any inquiries, contact support.
3330 </p>
3331 </div>',
3332 esc_html($plugin_name)
3333 );
3334 }
3335 flush_rewrite_rules();
3336 }
3337
3338 /**
3339 * Display update warning banner if plugin update is available
3340 * Checks WordPress update API to see if a newer version is available
3341 *
3342 * @since 1.0.0
3343 */
3344 public function display_update_warning_banner() {
3345 // Get the installed version from database
3346 $installed_version = get_option('metasync_version', '0.0.0');
3347
3348 // Get plugin basename for WordPress update API check
3349 // This is the plugin file path relative to plugins directory (e.g., 'metasync/metasync.php')
3350 $plugin_file = plugin_basename(plugin_dir_path(dirname(__FILE__)) . 'metasync.php');
3351
3352 // Get WordPress update plugins transient (contains available updates)
3353 $update_plugins = get_site_transient('update_plugins');
3354
3355 // Check if update information exists and if our plugin has an update available
3356 if ($update_plugins && isset($update_plugins->response) && isset($update_plugins->response[$plugin_file])) {
3357 $update_info = $update_plugins->response[$plugin_file];
3358 $latest_version = isset($update_info->new_version) ? $update_info->new_version : '';
3359
3360 // Compare installed version with latest available version
3361 if ($latest_version && version_compare($installed_version, $latest_version, '<')) {
3362 // Get the plugin name using centralized method
3363 $plugin_name = Metasync::get_effective_plugin_name();
3364
3365 // Show admin notice with plugin name included in the message
3366 printf(
3367 '<div class="notice notice-error is-dismissible">
3368 <p>
3369 <b>Warning from %s</b><br>
3370 A new version of %s is available. Please update to the latest version to ensure compatibility and access new features.
3371 For any inquiries, contact support.
3372 </p>
3373 </div>',
3374 esc_html($plugin_name),
3375 esc_html($plugin_name)
3376 );
3377 }
3378 }
3379 }
3380
3381
3382 /*
3383 Method to handle Ajax request from "Indexation Control" page
3384 */
3385 public function meta_sync_save_seo_controls() {
3386 Metasync_Settings_Registration::instance()->meta_sync_save_seo_controls();
3387 }
3388
3389 /**
3390 * Schedule transient cleanup cron job
3391 * Runs daily to clean up expired transients and reduce database load
3392 */
3393 public function schedule_transient_cleanup_cron()
3394 {
3395 // Clear any existing scheduled event first
3396 $this->unschedule_transient_cleanup_cron();
3397
3398 // Schedule new cron job daily
3399 if (!wp_next_scheduled('metasync_cleanup_transients')) {
3400 $scheduled = wp_schedule_event(time(), 'metasync_daily_cleanup', 'metasync_cleanup_transients');
3401
3402 if (!$scheduled) {
3403 error_log('MetaSync: Failed to schedule transient cleanup cron job');
3404 }
3405 }
3406 }
3407
3408 /**
3409 * Unschedule transient cleanup cron job
3410 */
3411 public function unschedule_transient_cleanup_cron()
3412 {
3413 $timestamp = wp_next_scheduled('metasync_cleanup_transients');
3414 if ($timestamp) {
3415 wp_unschedule_event($timestamp, 'metasync_cleanup_transients');
3416 error_log('MetaSync: Transient cleanup cron job unscheduled');
3417 }
3418 }
3419
3420 /**
3421 * Maybe schedule transient cleanup cron job
3422 * Called on init hook - always schedules for database maintenance
3423 */
3424 public function maybe_schedule_transient_cleanup_cron()
3425 {
3426 if (!wp_next_scheduled('metasync_cleanup_transients')) {
3427 $this->schedule_transient_cleanup_cron();
3428 }
3429 }
3430
3431 /**
3432 * Schedule hidden post manager cron job (runs every 7 days)
3433 * Called on init hook - always schedules for template checking
3434 */
3435 public function maybe_schedule_hidden_post_check()
3436 {
3437 if (!wp_next_scheduled('metasync_hidden_post_check')) {
3438 $scheduled = wp_schedule_event(time(), 'metasync_weekly', 'metasync_hidden_post_check');
3439
3440 if ($scheduled) {
3441 error_log('MetaSync: Hidden post manager cron job scheduled successfully (runs every 7 days)');
3442 } else {
3443 error_log('MetaSync: Failed to schedule hidden post manager cron job');
3444 }
3445 }
3446 }
3447
3448 /**
3449 * Schedule OTTO 404 exclusion recheck cron job (runs daily)
3450 * Rechecks URLs auto-excluded due to 404 after 7 days; removes from exclusion if now available
3451 */
3452 public function maybe_schedule_otto_recheck_404_cron()
3453 {
3454 if (!wp_next_scheduled('metasync_otto_recheck_404_exclusions')) {
3455 $scheduled = wp_schedule_event(time(), 'metasync_daily_cleanup', 'metasync_otto_recheck_404_exclusions');
3456 if ($scheduled) {
3457 error_log('MetaSync: OTTO 404 recheck cron job scheduled successfully (runs daily)');
3458 } else {
3459 error_log('MetaSync: Failed to schedule OTTO 404 recheck cron job');
3460 }
3461 }
3462 }
3463
3464 /**
3465 * Execute transient cleanup cron job
3466 * Cleans up expired transients and plugin-specific transients to reduce database load
3467 */
3468 public function execute_transient_cleanup()
3469 {
3470 Metasync_Admin_Ajax::instance()->execute_transient_cleanup();
3471 }
3472
3473 /**
3474 * Control plugin auto-updates based on user setting
3475 *
3476 * @param bool $update Whether to update
3477 * @param object $item Update offer
3478 * @return bool Whether to allow auto-update
3479 */
3480 public function control_plugin_auto_updates($update, $item)
3481 {
3482 # Check if the item object has the slug property
3483 if (!isset($item->slug)) {
3484 return $update;
3485 }
3486
3487 // Check if this is our plugin
3488 if ($item->slug === 'metasync') {
3489 $general_settings = Metasync::get_option('general') ?? [];
3490 $enable_auto_updates = $general_settings['enable_auto_updates'] ?? false;
3491
3492 // Return the user's preference (true = allow auto-updates, false = prevent)
3493 return $enable_auto_updates === 'true' || $enable_auto_updates === true;
3494 }
3495
3496 // For other plugins, don't interfere with their auto-update settings
3497 return $update;
3498 }
3499
3500 /**
3501 * AJAX handler to add excluded URL for OTTO
3502 */
3503 public function ajax_otto_add_excluded_url()
3504 {
3505 Metasync_Otto_Cache_Manager::instance()->ajax_otto_add_excluded_url();
3506 }
3507
3508 /**
3509 * AJAX handler to delete excluded URL for OTTO
3510 */
3511 public function ajax_otto_delete_excluded_url()
3512 {
3513 Metasync_Otto_Cache_Manager::instance()->ajax_otto_delete_excluded_url();
3514 }
3515
3516 /**
3517 * AJAX handler to recheck if an excluded URL is now available
3518 * Used for "Recheck" action on Excluded URLs list
3519 */
3520 public function ajax_otto_recheck_excluded_url()
3521 {
3522 Metasync_Otto_Cache_Manager::instance()->ajax_otto_recheck_excluded_url();
3523 }
3524
3525 /**
3526 * AJAX handler to get excluded URLs with pagination
3527 */
3528 public function ajax_otto_get_excluded_urls()
3529 {
3530 Metasync_Otto_Cache_Manager::instance()->ajax_otto_get_excluded_urls();
3531 }
3532
3533 /**
3534 * AJAX handler for submitting issue reports to Sentry
3535 *
3536 * @since 2.5.10
3537 * @return void Sends JSON response and exits
3538 */
3539 public function ajax_submit_issue_report()
3540 {
3541 Metasync_Admin_Ajax::instance()->ajax_submit_issue_report();
3542 }
3543
3544 /**
3545 * Format duration seconds into human-readable label
3546 *
3547 * @since 2.5.11
3548 * @param int $seconds Duration in seconds
3549 * @return string Human-readable duration
3550 */
3551 private function format_duration_label($seconds)
3552 {
3553 $labels = array(
3554 3600 => '1 hour',
3555 14400 => '4 hours',
3556 28800 => '8 hours',
3557 86400 => '24 hours',
3558 172800 => '48 hours',
3559 604800 => '7 days',
3560 1209600 => '14 days',
3561 2592000 => '30 days'
3562 );
3563
3564 if (isset($labels[$seconds])) {
3565 return $labels[$seconds];
3566 }
3567
3568 # Calculate hours if not a standard duration
3569 $hours = round($seconds / 3600);
3570 return $hours . ' hours';
3571 }
3572
3573 /**
3574 * AJAX handler for password recovery
3575 * Sends the whitelabel settings password to the configured recovery email
3576 */
3577 public function ajax_recover_password()
3578 {
3579 Metasync_Admin_Ajax::instance()->ajax_recover_password();
3580 }
3581
3582 /**
3583 * AJAX handler for saving theme preference
3584 * Saves the user's theme choice (light/dark) to WordPress options
3585 */
3586 public function ajax_save_theme()
3587 {
3588 Metasync_Admin_Ajax::instance()->ajax_save_theme();
3589 }
3590
3591 /**
3592 * AJAX handler for tracking 1-click activation in Mixpanel
3593 */
3594 public function ajax_track_one_click_activation()
3595 {
3596 Metasync_Admin_Ajax::instance()->ajax_track_one_click_activation();
3597 }
3598
3599 /**
3600 * Handler for exporting whitelabel settings to a zip file
3601 * Uses admin-post action for file downloads
3602 */
3603 public function handle_export_whitelabel_settings()
3604 {
3605 Metasync_Admin_Ajax::instance()->handle_export_whitelabel_settings();
3606 }
3607
3608
3609 /**
3610 * Add custom column to posts/pages list for HTML-converted pages
3611 *
3612 * @param array $columns Existing columns
3613 * @return array Modified columns
3614 */
3615 public function add_html_converted_column($columns)
3616 {
3617 // Add column after the title column
3618 $new_columns = array();
3619 foreach ($columns as $key => $value) {
3620 $new_columns[$key] = $value;
3621 if ($key === 'title') {
3622 $new_columns['metasync_html_source'] = __('Source', 'metasync');
3623 }
3624 }
3625 return $new_columns;
3626 }
3627
3628 /**
3629 * Render content for the HTML-converted column
3630 *
3631 * @param string $column_name Name of the column
3632 * @param int $post_id Post ID
3633 */
3634 public function render_html_converted_column($column_name, $post_id)
3635 {
3636 if ($column_name !== 'metasync_html_source') {
3637 return;
3638 }
3639
3640 // Check if this is an HTML-converted page
3641 $has_raw_html = get_post_meta($post_id, '_metasync_raw_html_enabled', true);
3642 $has_custom_css = get_post_meta($post_id, '_metasync_custom_css', true);
3643
3644 // If page has raw HTML or custom CSS from conversion, show badge
3645 if ($has_raw_html || !empty($has_custom_css)) {
3646 $label = $this->get_html_source_label();
3647 $tooltip = sprintf(
3648 __('This page was created using %s HTML-to-Builder converter', 'metasync'),
3649 $label
3650 );
3651
3652 echo sprintf(
3653 '<span class="metasync-html-badge" title="%s">
3654 <span class="metasync-badge-icon">⚡</span>
3655 <span class="metasync-badge-text">%s</span>
3656 </span>',
3657 esc_attr($tooltip),
3658 esc_html($label)
3659 );
3660 }
3661 }
3662
3663 /**
3664 * Get the label for HTML-converted pages (respects whitelabel settings)
3665 *
3666 * @return string Label to display
3667 */
3668 private function get_html_source_label()
3669 {
3670 $whitelabel_company = Metasync::get_whitelabel_company_name();
3671 if (!empty($whitelabel_company)) {
3672 return $whitelabel_company . ' AI';
3673 }
3674 return 'SearchAtlas AI';
3675 }
3676
3677 /**
3678 * Add source notice banner in the page editor
3679 *
3680 * @param WP_Post $post Current post object
3681 */
3682 public function add_editor_source_notice($post)
3683 {
3684 if (!$post || !in_array($post->post_type, array('post', 'page'))) {
3685 return;
3686 }
3687
3688 $has_raw_html = get_post_meta($post->ID, '_metasync_raw_html_enabled', true);
3689 $has_custom_css = get_post_meta($post->ID, '_metasync_custom_css', true);
3690
3691 if ($has_raw_html || !empty($has_custom_css)) {
3692 $label = $this->get_html_source_label();
3693 $message = sprintf(
3694 __('This page was created using %s HTML-to-Builder converter. The design is preserved with custom CSS and inline styles.', 'metasync'),
3695 '<strong>' . esc_html($label) . '</strong>'
3696 );
3697
3698 echo sprintf(
3699 '<div class="metasync-editor-notice notice notice-info is-dismissible">
3700 <div class="metasync-editor-notice-content">
3701 <span class="metasync-editor-badge">
3702 <span class="metasync-badge-icon">⚡</span>
3703 <span class="metasync-badge-text">%s</span>
3704 </span>
3705 <p class="metasync-editor-message">%s</p>
3706 </div>
3707 </div>',
3708 esc_html($label),
3709 $message
3710 );
3711 }
3712 }
3713
3714 /**
3715 * Add source display in quick edit panel
3716 *
3717 * @param string $column_name Column name
3718 * @param string $post_type Post type
3719 */
3720 public function add_quick_edit_source_display($column_name, $post_type)
3721 {
3722 if ($column_name !== 'metasync_html_source') {
3723 return;
3724 }
3725
3726 if (!in_array($post_type, array('post', 'page'))) {
3727 return;
3728 }
3729
3730 ?>
3731 <fieldset class="inline-edit-col-left metasync-quick-edit-source">
3732 <div class="inline-edit-col">
3733 <label>
3734 <span class="title"><?php _e('Source', 'metasync'); ?></span>
3735 <span class="metasync-quick-edit-badge-container"></span>
3736 </label>
3737 </div>
3738 </fieldset>
3739 <?php
3740 }
3741
3742 /**
3743 * Add dashboard widget for HTML-converted pages
3744 */
3745 public function add_html_pages_dashboard_widget()
3746 {
3747 $label = $this->get_html_source_label();
3748 $widget_title = sprintf(__('%s Pages', 'metasync'), $label);
3749
3750 wp_add_dashboard_widget(
3751 'metasync_html_pages_widget',
3752 $widget_title,
3753 array($this, 'render_html_pages_dashboard_widget')
3754 );
3755 }
3756
3757 /**
3758 * Render the dashboard widget content
3759 */
3760 public function render_html_pages_dashboard_widget()
3761 {
3762 Metasync_Admin_Ajax::instance()->render_html_pages_dashboard_widget();
3763 }
3764
3765 /**
3766 * Bot Statistics page callback
3767 * Displays bot detection statistics and logs
3768 */
3769 public function create_admin_bot_statistics_page()
3770 {
3771 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-otto-bot-statistics.php';
3772 }
3773
3774 /**
3775 * AJAX handler for resetting bot statistics
3776 */
3777 public function ajax_reset_bot_stats()
3778 {
3779 check_ajax_referer('metasync_reset_bot_stats', 'nonce');
3780
3781 if (!Metasync::current_user_has_plugin_access()) {
3782 wp_send_json_error(['message' => 'Insufficient permissions.']);
3783 }
3784
3785 require_once plugin_dir_path(dirname(__FILE__)) . 'otto/class-metasync-otto-bot-statistics-database.php';
3786 $db = Metasync_Otto_Bot_Statistics_Database::get_instance();
3787
3788 $result = $db->reset_statistics();
3789
3790 if ($result) {
3791 wp_send_json_success(['message' => 'Statistics reset successfully.']);
3792 } else {
3793 wp_send_json_error(['message' => 'Failed to reset statistics.']);
3794 }
3795 }
3796
3797 /**
3798 * AJAX handler for sending URLs to Google Instant Indexing API
3799 *
3800 * @since 2.6.0
3801 * @return void Sends JSON response and exits
3802 */
3803 public function ajax_send_giapi()
3804 {
3805 $post_data = metasync_sanitize_input_array($_POST);
3806 if (!isset($post_data['metasync_giapi_url'])) {
3807 return;
3808 }
3809
3810 // Parse URLs from textarea input (one per line)
3811 $urls = array_values(array_filter(array_map('trim', explode("\n", sanitize_textarea_field(wp_unslash($post_data['metasync_giapi_url']))))));
3812
3813 if (empty($urls)) {
3814 return;
3815 }
3816
3817 if (!isset($post_data['metasync_giapi_action'])) {
3818 return;
3819 }
3820 $action = sanitize_title($post_data['metasync_giapi_action']);
3821
3822 // Map form action values to google_index_direct action values
3823 if ($action === 'remove') {
3824 $action = 'delete';
3825 }
3826
3827 header('Content-type: application/json');
3828
3829 $result_data = [];
3830 foreach ($urls as $i => $url) {
3831 $url = esc_url_raw($url);
3832 if (empty($url)) {
3833 continue;
3834 }
3835
3836 if ($action === 'status') {
3837 $result = google_index_direct()->get_url_status($url);
3838 } else {
3839 $result = google_index_direct()->index_url($url, $action);
3840 }
3841
3842 $key = 'url-' . $i;
3843 if (!empty($result['success'])) {
3844 $result_data[$key] = $result['data'];
3845 } else {
3846 $result_data[$key] = (object) [
3847 'error' => (object) [
3848 'code' => isset($result['error']['code']) ? $result['error']['code'] : 400,
3849 'message' => isset($result['error']['message']) ? $result['error']['message'] : 'Unknown error',
3850 ]
3851 ];
3852 }
3853 }
3854
3855 // For single URL, unwrap from the batch format (matches old behavior)
3856 if (count($result_data) === 1) {
3857 $result_data = reset($result_data);
3858 }
3859
3860 wp_send_json($result_data);
3861 wp_die();
3862 }
3863
3864 /**
3865 * AJAX handler for sending URLs to Bing via IndexNow API
3866 *
3867 * @since 2.6.0
3868 * @return void Sends JSON response and exits
3869 */
3870 public function ajax_send_bing_indexnow()
3871 {
3872 require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php';
3873 $bing_instant_index = new Metasync_Bing_Instant_Index();
3874 $bing_instant_index->send();
3875 }
3876
3877 /**
3878 * Save instant indexing settings (Google and Bing)
3879 *
3880 * @since 2.6.0
3881 * @return void
3882 */
3883 public function save_instant_indexing_settings()
3884 {
3885 // Check if this is a settings submission
3886 if (!isset($_POST['submit'])) {
3887 return;
3888 }
3889
3890 // Save Google Instant Indexing settings
3891 if (isset($_POST['metasync_google_json_key'])) {
3892 $post_data = metasync_sanitize_input_array($_POST);
3893 $settings = get_option('metasync_options_instant_indexing', ['json_key' => '', 'post_types' => []]);
3894
3895 // Get JSON key from textarea or file upload
3896 $json = sanitize_textarea_field(wp_unslash($post_data['metasync_google_json_key']));
3897 if (isset($_FILES['metasync_google_json_file']) && !empty($_FILES['metasync_google_json_file']['tmp_name']) && file_exists($_FILES['metasync_google_json_file']['tmp_name'])) {
3898 $json = wp_unslash(file_get_contents($_FILES['metasync_google_json_file']['tmp_name']));
3899 }
3900
3901 $post_types = isset($post_data['metasync_post_types']) && is_array($post_data['metasync_post_types']) && !empty($json) ? array_map('sanitize_title', $post_data['metasync_post_types']) : [];
3902
3903 $new_settings = [
3904 'json_key' => $json,
3905 'post_types' => array_values($post_types),
3906 ];
3907
3908 $settings = array_merge($settings, $new_settings);
3909
3910 if (!empty($settings)) {
3911 update_option('metasync_options_instant_indexing', $settings);
3912 }
3913
3914 // Also save parsed credentials to google_index_direct for native API usage
3915 if (!empty($json) && function_exists('google_index_direct')) {
3916 $decoded = json_decode($json, true);
3917 if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
3918 google_index_direct()->save_service_account_config($decoded);
3919 }
3920 }
3921 }
3922
3923 // Note: Bing Instant Indexing settings are saved via AJAX in save_bing_inline_settings_ajax()
3924 }
3925
3926 /**
3927 * Save Bing instant indexing settings from inline form (Indexation Control page)
3928 *
3929 * @since 2.6.0
3930 * @return bool True on success, false on failure
3931 */
3932 private function save_bing_inline_settings_ajax() {
3933 return Metasync_Settings_Registration::instance()->save_bing_inline_settings_ajax();
3934 }
3935
3936 /**
3937 * Add instant indexing action links to post/page rows
3938 *
3939 * @since 2.6.0
3940 * @param array $actions Current actions
3941 * @param WP_Post $post Current post object
3942 * @return array Modified actions
3943 */
3944 public function add_instant_indexing_post_actions($actions, $post)
3945 {
3946 // Add Google Instant Indexing links
3947 $options = get_option('metasync_options_instant_indexing', ['json_key' => '', 'post_types' => []]);
3948 $post_types = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : [];
3949
3950 if (in_array($post->post_type, $post_types) && $post->post_status == 'publish') {
3951 $link = get_permalink($post);
3952
3953 // Get menu slug (support white label)
3954 $general_options = Metasync::get_option('general') ?? [];
3955 $menu_slug = !empty($general_options['white_label_plugin_menu_slug']) ? $general_options['white_label_plugin_menu_slug'] : 'searchatlas';
3956 $page_slug = $menu_slug . '-google-console';
3957
3958 $actions['index-update'] = '<a href="' . admin_url("admin.php?page=" . $page_slug . "&postaction=update&posturl=" . rawurlencode($link)) . '" title="" rel="permalink">Update Google Index</a>';
3959 $actions['index-status'] = '<a href="' . admin_url("admin.php?page=" . $page_slug . "&postaction=status&posturl=" . rawurlencode($link)) . '" title="" rel="permalink">Status Google Index</a>';
3960 }
3961
3962 // Add Bing Instant Indexing links
3963 require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php';
3964 $bing_instant_index = new Metasync_Bing_Instant_Index();
3965 $actions = $bing_instant_index->bing_instant_index_post_link($actions, $post);
3966
3967 return $actions;
3968 }
3969
3970 /**
3971 * Auto-submit post to instant indexing services when published
3972 *
3973 * @since 2.6.0
3974 * @param int $post_id Post ID
3975 * @param WP_Post $post Post object
3976 * @param bool $update Whether this is an update
3977 * @return void
3978 */
3979 public function auto_submit_to_instant_indexing($post_id, $post, $update)
3980 {
3981 // Google Instant Indexing auto-submit is not yet implemented via google_index_direct().
3982 // The previous implementation was also a no-op for Google.
3983 // TODO: Implement async auto-submit via WP-Cron or Action Scheduler in a future phase.
3984
3985 // Auto-submit to Bing Instant Indexing
3986 require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php';
3987 $bing_instant_index = new Metasync_Bing_Instant_Index();
3988 $bing_instant_index->auto_submit_on_publish($post_id, $post, $update);
3989 }
3990 }
3991