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

class-metasync-connect-manager.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.6, at includes/class-metasync-connect-manager.php

995 lines 35.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) {
3 exit;
4 }
5
6 /**
7 * SearchAtlas Connect / SSO Authentication Manager
8 *
9 * Handles all Search Atlas connect flow, token generation/validation,
10 * JWT management, session management, and authentication reset logic.
11 *
12 * @package Metasync
13 * @subpackage Metasync/includes
14 */
15 class Metasync_Connect_Manager
16 {
17 private static $instance = null;
18
19 public static function instance()
20 {
21 if (null === self::$instance) {
22 self::$instance = new self();
23 }
24 return self::$instance;
25 }
26
27 private function __construct() {}
28
29 // ------------------------------------------------------------------
30 // Token context validation
31 // ------------------------------------------------------------------
32
33 public function validate_searchatlas_context($token_data)
34 {
35 if (isset($token_data['site_url']) && $token_data['site_url'] !== get_site_url()) {
36 return false;
37 }
38
39 return true;
40 }
41
42 /**
43 * Check if IP validation should be enforced
44 */
45 public function should_validate_ip()
46 {
47 $settings = Metasync::get_option('general');
48 return isset($settings['enforce_ip_validation']) ? (bool)$settings['enforce_ip_validation'] : false;
49 }
50
51 /**
52 * Check if user agents are incompatible (not just version differences)
53 */
54 public function are_user_agents_incompatible($old_ua, $new_ua)
55 {
56 $old_browser = $this->extract_browser_name($old_ua);
57 $new_browser = $this->extract_browser_name($new_ua);
58
59 return $old_browser !== $new_browser && !empty($old_browser) && !empty($new_browser);
60 }
61
62 /**
63 * Extract browser name from user agent string
64 */
65 public function extract_browser_name($ua)
66 {
67 if (stripos($ua, 'Chrome') !== false) return 'Chrome';
68 if (stripos($ua, 'Firefox') !== false) return 'Firefox';
69 if (stripos($ua, 'Safari') !== false) return 'Safari';
70 if (stripos($ua, 'Edge') !== false) return 'Edge';
71 if (stripos($ua, 'Opera') !== false) return 'Opera';
72 return 'Unknown';
73 }
74
75 // ------------------------------------------------------------------
76 // Plugin Auth Token helpers
77 // ------------------------------------------------------------------
78
79 /**
80 * Generate Search Atlas WordPress Connect Token.
81 *
82 * Returns the Plugin Auth Token used to authenticate with the Search Atlas platform
83 * during the 1-click connect flow. This token is used ONLY to retrieve the Search Atlas
84 * API key and Otto UUID — it does NOT log anyone into WordPress.
85 */
86 public function generate_searchatlas_wp_connect_token($regenerate = false)
87 {
88 $general_options = Metasync::get_option('general') ?? [];
89 $plugin_auth_token = $general_options['apikey'] ?? '';
90
91 if (empty($plugin_auth_token)) {
92 error_log('MetaSync ERROR: Plugin Auth Token missing from options - should have been generated during activation');
93 return false;
94 }
95
96 return $plugin_auth_token;
97 }
98
99 /**
100 * Ensure Plugin Auth Token exists before Search Atlas connect authentication.
101 * Auto-generates if missing to ensure smooth connect flow.
102 */
103 public function ensure_plugin_auth_token_exists()
104 {
105 $options = Metasync::get_option();
106 $current_plugin_auth_token = $options['general']['apikey'] ?? '';
107
108 if (empty($current_plugin_auth_token)) {
109
110 $new_plugin_auth_token = wp_generate_password(32, false, false);
111
112 if (!isset($options['general'])) {
113 $options['general'] = [];
114 }
115
116 $options['general']['apikey'] = $new_plugin_auth_token;
117
118 $save_result = Metasync::set_option($options);
119
120 if ($save_result) {
121 Metasync::log_api_key_event('auto_generated_for_sa_connect', 'plugin_auth_token', array(
122 'new_token_prefix' => substr($new_plugin_auth_token, 0, 8) . '...',
123 'triggered_by' => 'sa_connect_button',
124 'reason' => 'Plugin Auth Token was missing before Search Atlas connect authentication'
125 ), 'info');
126
127 } else {
128 global $wpdb;
129 if (class_exists('Metasync_Error_Logger') && !empty($wpdb->last_error)) {
130 Metasync_Error_Logger::log(
131 Metasync_Error_Logger::CATEGORY_DATABASE_ERROR,
132 Metasync_Error_Logger::SEVERITY_CRITICAL,
133 'Failed to save plugin auth token to database',
134 [
135 'option_name' => Metasync::option_name,
136 'wpdb_error' => $wpdb->last_error,
137 'wpdb_last_query' => $wpdb->last_query,
138 'operation' => 'ensure_plugin_auth_token_exists',
139 'triggered_by' => 'sso_connect_button'
140 ]
141 );
142 }
143
144 throw new Exception('Failed to generate required authentication token');
145 }
146 }
147 }
148
149 /**
150 * Refresh Plugin Auth Token (AJAX endpoint)
151 */
152 public function refresh_plugin_auth_token()
153 {
154 if (!wp_verify_nonce($_POST['nonce'], 'metasync_refresh_plugin_auth_token')) {
155 wp_send_json_error(array('message' => 'Invalid nonce'));
156 return;
157 }
158
159 if (!Metasync::current_user_has_plugin_access()) {
160 wp_send_json_error(array('message' => 'Insufficient permissions'));
161 return;
162 }
163
164 try {
165 $new_plugin_auth_token = wp_generate_password(32, false, false);
166
167 $options = Metasync::get_option();
168 if (!isset($options['general'])) {
169 $options['general'] = [];
170 }
171 $options['general']['apikey'] = $new_plugin_auth_token;
172
173 $save_result = Metasync::set_option($options);
174
175 if ($save_result) {
176 Metasync::log_api_key_event('token_refresh', 'plugin_auth_token', array(
177 'new_token_prefix' => substr($new_plugin_auth_token, 0, 8) . '...',
178 'triggered_by' => 'manual_refresh_button'
179 ), 'info');
180
181 do_action('metasync_trigger_immediate_heartbeat', 'Plugin Auth Token refresh - new token generated');
182
183 wp_send_json_success(array(
184 'new_token' => $new_plugin_auth_token,
185 'message' => 'Plugin Auth Token refreshed successfully'
186 ));
187 } else {
188 wp_send_json_error(array('message' => 'Failed to save new token'));
189 }
190
191 } catch (Exception $e) {
192 error_log('Plugin Auth Token Refresh Error: ' . $e->getMessage());
193 wp_send_json_error(array('message' => 'Error generating new token'));
194 }
195 }
196
197 /**
198 * Get current Plugin Auth Token (AJAX endpoint for UI updates)
199 */
200 public function get_plugin_auth_token()
201 {
202 if (!wp_verify_nonce($_POST['nonce'], 'metasync_sa_connect_nonce')) {
203 wp_send_json_error(array('message' => 'Invalid nonce'));
204 return;
205 }
206
207 if (!Metasync::current_user_has_plugin_access()) {
208 wp_send_json_error(array('message' => 'Insufficient permissions'));
209 return;
210 }
211
212 try {
213 $options = Metasync::get_option();
214 $current_plugin_auth_token = $options['general']['apikey'] ?? '';
215
216 if (!empty($current_plugin_auth_token)) {
217 wp_send_json_success(array(
218 'plugin_auth_token' => $current_plugin_auth_token,
219 'message' => 'Plugin Auth Token retrieved successfully'
220 ));
221 } else {
222 wp_send_json_error(array('message' => 'Plugin Auth Token not found'));
223 }
224
225 } catch (Exception $e) {
226 error_log('Get Plugin Auth Token Error: ' . $e->getMessage());
227 wp_send_json_error(array('message' => 'Error retrieving Plugin Auth Token'));
228 }
229 }
230
231 // ------------------------------------------------------------------
232 // Search Atlas Connect URL & polling
233 // ------------------------------------------------------------------
234
235 /**
236 * Generate Search Atlas Connect URL (1-click connect).
237 *
238 * AJAX action: wp_ajax_metasync_generate_connect_url
239 */
240 public function generate_searchatlas_connect_url()
241 {
242 if (!current_user_can('manage_options')) {
243 wp_send_json_error(array('message' => 'Insufficient permissions. Administrator access required.'));
244 return;
245 }
246
247 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'metasync_sa_connect_nonce')) {
248 wp_send_json_error(array('message' => 'Invalid nonce - please refresh the page and try again'));
249 return;
250 }
251
252 $rate_limit_key = 'metasync_sa_connect_rate_' . get_current_user_id();
253 $rate_limit_count = get_transient($rate_limit_key);
254 if ($rate_limit_count !== false && $rate_limit_count >= 10) {
255 wp_send_json_error(array('message' => 'Too many connect requests. Please wait a few minutes before trying again.'));
256 return;
257 }
258 set_transient($rate_limit_key, ($rate_limit_count === false ? 1 : $rate_limit_count + 1), 300);
259
260 try {
261 $this->ensure_plugin_auth_token_exists();
262
263 $sa_connect_token = $this->create_searchatlas_nonce_token();
264
265 if (!$sa_connect_token) {
266 wp_send_json_error(array('message' => 'Failed to create authentication token'));
267 return;
268 }
269
270 $domain = str_replace('://www.', '://', get_site_url());
271
272 $dashboard_domain = Metasync_Admin::get_effective_dashboard_domain();
273
274 $sa_connect_url = $dashboard_domain . '/sso/wordpress?' . http_build_query([
275 'nonce_token' => $sa_connect_token,
276 'domain' => $domain,
277 'return_url' => admin_url('admin.php?page=' . Metasync_Admin::$page_slug)
278 ]);
279
280 wp_send_json_success(array(
281 'connect_url' => $sa_connect_url,
282 'nonce_token' => $sa_connect_token,
283 'debug_info' => array(
284 'dashboard_domain' => $dashboard_domain,
285 'site_domain' => $domain,
286 'return_url' => admin_url('admin.php?page=' . Metasync_Admin::$page_slug)
287 )
288 ));
289
290 } catch (Exception $e) {
291 wp_send_json_error(array('message' => 'Failed to generate Search Atlas connect URL: ' . $e->getMessage()));
292 }
293 }
294
295 /**
296 * Check Search Atlas Connect Status (polling endpoint).
297 *
298 * AJAX action: wp_ajax_metasync_check_connect_status
299 */
300 public function check_searchatlas_connect_status()
301 {
302 if (!current_user_can('manage_options')) {
303 wp_send_json_error(array('message' => 'Insufficient permissions. Administrator access required.'));
304 return;
305 }
306
307 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'metasync_sa_connect_nonce')) {
308 wp_send_json_error(array('message' => 'Invalid nonce'));
309 return;
310 }
311
312 $nonce_token = isset($_POST['nonce_token']) ? sanitize_text_field(wp_unslash($_POST['nonce_token'])) : '';
313
314 // Check if THIS specific nonce was successfully processed
315 // This prevents false positives from background sync/heartbeat activity
316 $success_key = 'metasync_sa_connect_success_' . md5($nonce_token);
317 $this_auth_completed = get_transient($success_key);
318
319
320 if ($this_auth_completed) {
321 // Delete the transient (one-time use) to prevent replay
322 delete_transient($success_key);
323
324 // Get current settings to return API key
325 $general_settings = Metasync::get_option('general') ?? [];
326
327 wp_send_json_success(array(
328 'updated' => true,
329 'api_key' => $general_settings['searchatlas_api_key'], // Return full API key
330 'otto_pixel_uuid' => $general_settings['otto_pixel_uuid'] ?? '', // Return OTTO UUID for UI update
331 'status_code' => 200,
332 'whitelabel_enabled' => !empty($general_settings['white_label_plugin_name']),
333 'effective_domain' => Metasync_Admin::get_effective_dashboard_domain()
334 ));
335 }
336
337 wp_send_json_success(array('updated' => false));
338 }
339
340 // ------------------------------------------------------------------
341 // Nonce / encrypted token helpers
342 // ------------------------------------------------------------------
343
344 /**
345 * Create Search Atlas Connect Nonce Token.
346 *
347 * Generates a unique, time-limited (15 min), single-use nonce token used to
348 * identify the connect session when Search Atlas calls back with the API key
349 * and Otto UUID.
350 */
351 public function create_searchatlas_nonce_token()
352 {
353 $general_options = Metasync::get_option('general') ?? [];
354 $plugin_auth_token = $general_options['apikey'] ?? '';
355
356 if (empty($plugin_auth_token)) {
357 error_log('MetaSync ERROR: Plugin Auth Token missing from options');
358 return false;
359 }
360
361 $random_bytes = wp_generate_password(32, false, false);
362 $timestamp = time();
363 $user_id = get_current_user_id();
364
365 $token_data = $random_bytes . '|' . $timestamp . '|' . $user_id . '|' . get_site_url();
366 $sa_connect_token = hash_hmac('sha256', $token_data, $plugin_auth_token . wp_salt('auth'));
367
368 $token_metadata = array(
369 'created' => $timestamp,
370 'expires' => $timestamp + 900,
371 'user_id' => $user_id,
372 'site_url' => get_site_url(),
373 'ip' => $this->get_client_ip(),
374 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? substr(sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])), 0, 100) : '',
375 'used' => false,
376 'callback_used' => false,
377 'version' => '3.0'
378 );
379
380 $transient_key = 'metasync_sa_connect_token_' . substr(hash('sha256', $sa_connect_token), 0, 32);
381 set_transient($transient_key, $token_metadata, 900);
382
383 set_transient('metasync_sa_connect_active_' . $sa_connect_token, $transient_key, 900);
384
385 return $sa_connect_token;
386 }
387
388 /**
389 * Get client IP address securely
390 */
391 public function get_client_ip()
392 {
393 $ip_headers = array('HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR');
394
395 foreach ($ip_headers as $header) {
396 if (!empty($_SERVER[$header])) {
397 $ip = $_SERVER[$header];
398 if (strpos($ip, ',') !== false) {
399 $ip = trim(explode(',', $ip)[0]);
400 }
401 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
402 return $ip;
403 }
404 }
405 }
406
407 return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
408 }
409
410 /**
411 * Create encrypted Search Atlas connect token with embedded metadata
412 */
413 public function create_encrypted_searchatlas_token($metadata = array())
414 {
415 $payload = array_merge(array(
416 'iat' => time(),
417 'exp' => time() + 1800,
418 'iss' => get_site_url(),
419 'aud' => 'search-atlas-connect',
420 'sub' => 'searchatlas-authentication',
421 'jti' => wp_generate_password(16, false),
422 'nonce' => wp_generate_password(16, false),
423 'version' => '2.0'
424 ), $metadata);
425
426 return $this->wp_encrypt_token($payload);
427 }
428
429 /**
430 * Encrypt token using WordPress SALTs
431 */
432 public function wp_encrypt_token($payload)
433 {
434 try {
435 $serialized = serialize($payload);
436
437 $key_material = wp_salt('secure_auth') . wp_salt('logged_in') . wp_salt('nonce');
438 $encryption_key = hash('sha256', $key_material, true);
439
440 $iv = random_bytes(16);
441
442 $encrypted = openssl_encrypt($serialized, 'AES-256-CBC', $encryption_key, OPENSSL_RAW_DATA, $iv);
443
444 if ($encrypted === false) {
445 throw new Exception('Encryption failed');
446 }
447
448 $result = $iv . $encrypted;
449
450 return base64_encode($result);
451
452 } catch (Exception $e) {
453 return false;
454 }
455 }
456
457 /**
458 * Decrypt token using WordPress SALTs
459 */
460 public function wp_decrypt_token($encrypted_token)
461 {
462 try {
463 $data = base64_decode($encrypted_token, true);
464
465 if ($data === false || strlen($data) < 16) {
466 return false;
467 }
468
469 $iv = substr($data, 0, 16);
470 $encrypted = substr($data, 16);
471
472 $key_material = wp_salt('secure_auth') . wp_salt('logged_in') . wp_salt('nonce');
473 $encryption_key = hash('sha256', $key_material, true);
474
475 $serialized = openssl_decrypt($encrypted, 'AES-256-CBC', $encryption_key, OPENSSL_RAW_DATA, $iv);
476
477 if ($serialized === false) {
478 return false;
479 }
480
481 $payload = unserialize($serialized, ['allowed_classes' => false]); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
482
483 if (!is_array($payload) || !isset($payload['exp'], $payload['iat'])) {
484 return false;
485 }
486
487 if ($payload['exp'] < time()) {
488 return false;
489 }
490
491 return $payload;
492
493 } catch (Exception $e) {
494 return false;
495 }
496 }
497
498 // ------------------------------------------------------------------
499 // Cleanup helpers
500 // ------------------------------------------------------------------
501
502 /**
503 * @deprecated No longer needed with simplified token system
504 */
505 public function cleanup_searchatlas_nonce_tokens()
506 {
507 return 0;
508 }
509
510 /**
511 * Cleanup Search Atlas connect rate limiting data
512 */
513 public function cleanup_searchatlas_rate_limits()
514 {
515 global $wpdb;
516
517 try {
518 $rate_limit_transients = $wpdb->get_results(
519 "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE '_transient_sa_connect_rate_limit_%'",
520 ARRAY_A
521 );
522
523 $cleaned_count = 0;
524
525 foreach ($rate_limit_transients as $transient) {
526 $transient_name = str_replace('_transient_', '', $transient['option_name']);
527 delete_transient($transient_name);
528 $cleaned_count++;
529 }
530
531 return $cleaned_count;
532
533 } catch (Exception $e) {
534 return 0;
535 }
536 }
537
538 /**
539 * Clear cached JWT tokens
540 */
541 public function clear_jwt_token_cache()
542 {
543 global $wpdb;
544
545 $deleted = $wpdb->query(
546 $wpdb->prepare(
547 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
548 '_transient_metasync_jwt_token_%'
549 )
550 );
551
552 $wpdb->query(
553 $wpdb->prepare(
554 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
555 '_transient_timeout_metasync_jwt_token_%'
556 )
557 );
558 }
559
560 // ------------------------------------------------------------------
561 // JWT token management
562 // ------------------------------------------------------------------
563
564 /**
565 * Get active JWT token for the plugin.
566 * Public static method accessible from anywhere in the plugin.
567 *
568 * @param bool $force_refresh Force generation of new token even if cached one exists
569 * @return string|false JWT token on success, false on failure
570 */
571 public static function get_active_jwt_token($force_refresh = false)
572 {
573 $general_options = Metasync::get_option('general') ?? [];
574 $api_key = $general_options['searchatlas_api_key'] ?? '';
575
576 if (empty($api_key)) {
577 return false;
578 }
579
580 if (!$force_refresh) {
581 $cache_key = 'metasync_jwt_token_' . md5($api_key);
582 $cached_token_data = get_transient($cache_key);
583
584 if ($cached_token_data && is_array($cached_token_data)) {
585 $expires_with_buffer = $cached_token_data['expires'] - 300;
586 if (time() < $expires_with_buffer && !empty($cached_token_data['token'])) {
587 return $cached_token_data['token'];
588 }
589 }
590 }
591
592 return self::instance()->get_fresh_jwt_token();
593 }
594
595 /**
596 * Get fresh JWT token from Search Atlas API with caching
597 *
598 * @return string|false JWT token on success, false on failure
599 */
600 public function get_fresh_jwt_token()
601 {
602 $general_options = Metasync::get_option('general') ?? [];
603 $api_key = $general_options['searchatlas_api_key'] ?? '';
604
605 if (empty($api_key)) {
606 return false;
607 }
608
609 $cache_key = 'metasync_jwt_token_' . md5($api_key);
610 $cached_token_data = get_transient($cache_key);
611
612 if ($cached_token_data && is_array($cached_token_data)) {
613 $expires_with_buffer = $cached_token_data['expires'] - 300;
614 if (time() < $expires_with_buffer && !empty($cached_token_data['token'])) {
615 return $cached_token_data['token'];
616 }
617 }
618
619 $api_domain = class_exists('Metasync_Endpoint_Manager')
620 ? Metasync_Endpoint_Manager::get_endpoint('API_DOMAIN')
621 : Metasync::API_DOMAIN;
622 $url = $api_domain . '/api/customer/account/generate-jwt-from-api-key/';
623
624 $args = array(
625 'method' => 'POST',
626 'headers' => array(
627 'X-API-KEY' => $api_key,
628 'Content-Type' => 'application/json'
629 ),
630 'timeout' => 15
631 );
632
633 try {
634 $response = wp_remote_post($url, $args);
635
636 if (is_wp_error($response)) {
637 error_log('MetaSync: JWT token API request failed - ' . $response->get_error_message());
638 return false;
639 }
640
641 $response_code = wp_remote_retrieve_response_code($response);
642 $response_body = wp_remote_retrieve_body($response);
643
644 if ($response_code !== 200) {
645 error_log('MetaSync: JWT token API returned error code ' . $response_code);
646 return false;
647 }
648
649 $data = json_decode($response_body, true);
650
651 if (!$data || !isset($data['token'], $data['expires'])) {
652 error_log('MetaSync: Invalid JWT token API response format');
653 return false;
654 }
655
656 $token_data = array(
657 'token' => $data['token'],
658 'expires' => $data['expires'],
659 'created_at' => time()
660 );
661
662 $cache_duration = min($data['expires'] - time(), 24 * 3600);
663 set_transient($cache_key, $token_data, $cache_duration);
664
665 return $data['token'];
666
667 } catch (Exception $e) {
668 error_log('MetaSync: Exception during JWT generation - ' . $e->getMessage());
669 return false;
670 }
671 }
672
673 // ------------------------------------------------------------------
674 // Authentication reset
675 // ------------------------------------------------------------------
676
677 /**
678 * Reset Search Atlas Authentication
679 * Clears all authentication data and tokens
680 */
681 public function reset_searchatlas_authentication()
682 {
683 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'metasync_reset_auth_nonce')) {
684 wp_send_json_error(array(
685 'message' => 'Security verification failed. Please refresh the page and try again.',
686 'code' => 'invalid_nonce'
687 ));
688 return;
689 }
690
691 if (!current_user_can('manage_options')) {
692 wp_send_json_error(array(
693 'message' => 'You do not have permission to reset authentication.',
694 'code' => 'insufficient_permissions'
695 ));
696 return;
697 }
698
699 try {
700 $options = Metasync::get_option();
701
702 if (!is_array($options)) {
703 $options = array();
704 }
705
706 if (!isset($options['general'])) {
707 $options['general'] = array();
708 }
709
710 $cleared_data = array();
711
712 if (isset($options['general']['searchatlas_api_key'])) {
713 $cleared_data['searchatlas_api_key'] = substr($options['general']['searchatlas_api_key'], 0, 8) . '...';
714 unset($options['general']['searchatlas_api_key']);
715 }
716
717 if (isset($options['general']['otto_pixel_uuid'])) {
718 $cleared_data['otto_pixel_uuid'] = $options['general']['otto_pixel_uuid'];
719 unset($options['general']['otto_pixel_uuid']);
720 }
721
722 if (isset($options['general']['send_auth_token_timestamp'])) {
723 $cleared_data['send_auth_token_timestamp'] = $options['general']['send_auth_token_timestamp'];
724 unset($options['general']['send_auth_token_timestamp']);
725 }
726
727 if (isset($options['general']['last_heart_beat'])) {
728 $cleared_data['last_heart_beat'] = $options['general']['last_heart_beat'];
729 unset($options['general']['last_heart_beat']);
730 }
731
732 $save_result = Metasync::set_option($options);
733
734 if (!$save_result) {
735 throw new Exception('Failed to save updated plugin options');
736 }
737
738 delete_option('metasync_wp_sa_connect_token');
739 $cleared_data['wp_sa_connect_token'] = 'removed';
740
741 $cleaned_tokens = $this->cleanup_searchatlas_nonce_tokens();
742 $cleared_data['sa_connect_nonce_tokens'] = 'none (simplified token system)';
743
744 delete_option(Metasync::option_name . '_whitelabel_user');
745 $cleared_data['whitelabel_user'] = 'removed';
746
747 if (isset($options['whitelabel'])) {
748 $cleared_data['whitelabel_settings'] = 'removed';
749 unset($options['whitelabel']);
750
751 Metasync::set_option($options);
752 }
753
754 $this->clear_jwt_token_cache();
755 $cleared_data['jwt_token_cache'] = 'cleared';
756
757 $this->cleanup_searchatlas_rate_limits();
758 $cleared_data['rate_limits'] = 'cleared';
759
760 $otto_uuid = $cleared_data['otto_pixel_uuid'] ?? '';
761 if (!empty($otto_uuid)) {
762 delete_transient(Metasync_Heartbeat_Manager::public_hash_cache_key($otto_uuid));
763 }
764 $cleared_data['public_hash_cache'] = 'cleared';
765
766 delete_transient('metasync_heartbeat_status_cache');
767 $cleared_data['heartbeat_cache'] = 'cleared';
768
769 Metasync_Heartbeat_Manager::instance()->unschedule_heartbeat_cron();
770
771 wp_send_json_success(array(
772 'message' => 'Authentication has been reset successfully. You can now connect a new account.',
773 'cleared_data' => $cleared_data,
774 'timestamp' => current_time('mysql', true)
775 ));
776
777 } catch (Exception $e) {
778 error_log('Authentication Reset Error: ' . $e->getMessage());
779 wp_send_json_error(array(
780 'message' => 'An error occurred while resetting authentication. Please try again or contact support.',
781 'code' => 'reset_failed',
782 'error' => $e->getMessage()
783 ));
784 }
785 }
786
787 // ------------------------------------------------------------------
788 // Test / debug endpoints
789 // ------------------------------------------------------------------
790
791 /**
792 * Test the enhanced Search Atlas connect token system (development/debugging)
793 */
794 public function test_enhanced_searchatlas_tokens()
795 {
796 if (!current_user_can('manage_options')) {
797 return false;
798 }
799
800 $general_options = Metasync::get_option('general') ?? [];
801 $test_token = $general_options['apikey'] ?? null;
802
803 $apikey = $general_options['apikey'] ?? '';
804
805 $encrypted_token = $this->create_encrypted_searchatlas_token(['test' => 'data', 'user_id' => get_current_user_id()]);
806 if ($encrypted_token) {
807 $decrypted = $this->wp_decrypt_token($encrypted_token);
808 }
809
810 return true;
811 }
812
813 /**
814 * Test Search Atlas connect AJAX endpoint (development/debugging)
815 */
816 public function test_searchatlas_ajax_endpoint()
817 {
818 if (!current_user_can('manage_options')) {
819 wp_send_json_error(array(
820 'message' => 'Insufficient permissions for AJAX test',
821 'required_capability' => 'manage_options'
822 ));
823 return;
824 }
825
826 $nonce_valid = false;
827 if (isset($_POST['nonce'])) {
828 $nonce_valid = wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'metasync_sa_connect_nonce');
829 }
830
831 wp_send_json_success(array(
832 'message' => 'AJAX endpoint is working correctly',
833 'timestamp' => current_time('mysql', true),
834 'user_id' => get_current_user_id(),
835 'endpoint' => 'test_searchatlas_ajax_endpoint',
836 'nonce_valid' => $nonce_valid,
837 'debug_info' => array(
838 'post_action' => isset($_POST['action']) ? sanitize_text_field(wp_unslash($_POST['action'])) : 'NOT SET',
839 'has_nonce' => isset($_POST['nonce']),
840 'user_can_manage_options' => current_user_can('manage_options')
841 )
842 ));
843 }
844
845 /**
846 * Simple AJAX test without nonce (for debugging connectivity)
847 */
848 public function simple_ajax_test()
849 {
850 wp_send_json_success(array(
851 'message' => 'Basic AJAX connectivity works',
852 'timestamp' => time(),
853 'no_nonce_required' => true
854 ));
855 }
856
857 /**
858 * Test whitelabel domain configuration (development/debugging)
859 */
860 public function test_whitelabel_domain()
861 {
862 if (!current_user_can('manage_options')) {
863 wp_send_json_error('Administrator access required');
864 return;
865 }
866
867 $whitelabel_settings = Metasync::get_whitelabel_settings();
868
869 $is_enabled = Metasync::is_whitelabel_enabled();
870
871 $effective_domain = Metasync_Admin::get_effective_dashboard_domain();
872 $metasync_domain = Metasync::get_dashboard_domain();
873
874 $whitelabel_logo = Metasync::get_whitelabel_logo();
875
876 $default_domain = Metasync::DASHBOARD_DOMAIN;
877
878 $whitelabel_company_name = Metasync::get_whitelabel_company_name();
879
880 $effective_plugin_name = Metasync::get_effective_plugin_name('Test Plugin');
881
882 wp_send_json_success(array(
883 'whitelabel_settings' => $whitelabel_settings,
884 'is_enabled' => $is_enabled,
885 'effective_domain' => $effective_domain,
886 'whitelabel_logo' => $whitelabel_logo,
887 'whitelabel_company_name' => $whitelabel_company_name,
888 'effective_plugin_name' => $effective_plugin_name,
889 'default_domain' => $default_domain,
890 'override_active' => $effective_domain !== $default_domain
891 ));
892 }
893
894 // ------------------------------------------------------------------
895 // Whitelabel session / password management
896 // ------------------------------------------------------------------
897
898 /**
899 * Handle session management early in the admin lifecycle
900 */
901 public function handle_session_management_early()
902 {
903 if (!is_admin()) {
904 return;
905 }
906
907 $active_tab = isset($_GET['tab']) ? $_GET['tab'] : 'general';
908 $current_page = isset($_GET['page']) ? $_GET['page'] : '';
909
910 $whitelabel_settings = Metasync::get_whitelabel_settings();
911 $user_password = $whitelabel_settings['settings_password'] ?? '';
912 $hide_settings_enabled = !empty($whitelabel_settings['hide_settings']);
913
914 $protected_tabs = [];
915 if (!empty($user_password)) {
916 $protected_tabs[] = 'whitelabel';
917 }
918 if ($hide_settings_enabled && !empty($user_password)) {
919 $protected_tabs = ['general', 'whitelabel', 'advanced'];
920 }
921
922 if (strpos($current_page, Metasync_Admin::$page_slug) === 0 && in_array($active_tab, $protected_tabs)) {
923 if ((defined('REST_REQUEST') && REST_REQUEST) ||
924 (defined('DOING_AJAX') && DOING_AJAX) ||
925 (defined('DOING_CRON') && DOING_CRON)) {
926 return;
927 }
928
929 $this->handle_whitelabel_session_logic();
930 }
931 }
932
933 /**
934 * Handle whitelabel authentication logic (login/logout/validation)
935 * Uses Metasync_Auth_Manager instead of sessions for better compatibility
936 */
937 public function handle_whitelabel_session_logic()
938 {
939 $auth = new Metasync_Auth_Manager('whitelabel', 1800);
940
941 $admin_password = 'abracadabra@2020';
942
943 $whitelabel_settings = Metasync::get_whitelabel_settings();
944 $user_password = $whitelabel_settings['settings_password'] ?? '';
945
946 $valid_passwords = array($admin_password);
947 if (!empty($user_password)) {
948 $valid_passwords[] = $user_password;
949 }
950
951 if (isset($_POST['whitelabel_logout'])) {
952 if (wp_verify_nonce($_POST['whitelabel_logout_nonce'] ?? '', 'whitelabel_logout_nonce')) {
953 $auth->revoke_access();
954
955 $redirect_tab = $_GET['tab'] ?? 'whitelabel';
956 $redirect_url = admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '&tab=' . $redirect_tab);
957 wp_redirect($redirect_url);
958 exit;
959 }
960 }
961
962 if (isset($_POST['whitelabel_password_submit']) && isset($_POST['whitelabel_password'])) {
963 if (wp_verify_nonce($_POST['whitelabel_nonce'], 'whitelabel_password_nonce')) {
964 $submitted_password = sanitize_text_field($_POST['whitelabel_password']);
965
966 $auth->verify_and_grant($submitted_password, $valid_passwords, false);
967 }
968 }
969 }
970
971 /**
972 * Handle whitelabel password early before WordPress filters it out
973 */
974 public function handle_whitelabel_password_early()
975 {
976 if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['option_page']) && $_POST['option_page'] === Metasync_Admin::option_group) {
977
978 if (isset($_POST[Metasync_Admin::option_key]['whitelabel']['settings_password'])) {
979 $submitted_password = sanitize_text_field($_POST[Metasync_Admin::option_key]['whitelabel']['settings_password']);
980
981 $current_options = Metasync::get_option();
982
983 if (!isset($current_options['whitelabel'])) {
984 $current_options['whitelabel'] = [];
985 }
986
987 $current_options['whitelabel']['settings_password'] = $submitted_password;
988 $current_options['whitelabel']['updated_at'] = time();
989
990 update_option(Metasync_Admin::option_key, $current_options);
991 }
992 }
993 }
994 }
995