PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.7
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.7
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 / includes / class-metasync-connect-manager.php

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

997 lines 35.2 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 'callback_url' => get_rest_url(null, 'metasync/v1/searchatlas/connect/callback'),
278 'return_url' => admin_url('admin.php?page=' . Metasync_Admin::$page_slug)
279 ]);
280
281 wp_send_json_success(array(
282 'connect_url' => $sa_connect_url,
283 'nonce_token' => $sa_connect_token,
284 'debug_info' => array(
285 'dashboard_domain' => $dashboard_domain,
286 'site_domain' => $domain,
287 'return_url' => admin_url('admin.php?page=' . Metasync_Admin::$page_slug)
288 )
289 ));
290
291 } catch (Exception $e) {
292 wp_send_json_error(array('message' => 'Failed to generate Search Atlas connect URL: ' . $e->getMessage()));
293 }
294 }
295
296 /**
297 * Check Search Atlas Connect Status (polling endpoint).
298 *
299 * AJAX action: wp_ajax_metasync_check_connect_status
300 */
301 public function check_searchatlas_connect_status()
302 {
303 if (!current_user_can('manage_options')) {
304 wp_send_json_error(array('message' => 'Insufficient permissions. Administrator access required.'));
305 return;
306 }
307
308 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'metasync_sa_connect_nonce')) {
309 wp_send_json_error(array('message' => 'Invalid nonce'));
310 return;
311 }
312
313 $nonce_token = isset($_POST['nonce_token']) ? sanitize_text_field(wp_unslash($_POST['nonce_token'])) : '';
314
315 // Check if THIS specific nonce was successfully processed
316 // This prevents false positives from background sync/heartbeat activity
317 $success_key = 'metasync_sa_connect_success_' . md5($nonce_token);
318 $this_auth_completed = get_transient($success_key);
319
320
321 if ($this_auth_completed) {
322 // Delete the transient (one-time use) to prevent replay
323 delete_transient($success_key);
324
325 // Get current settings to return API key
326 $general_settings = Metasync::get_option('general') ?? [];
327
328 wp_send_json_success(array(
329 'updated' => true,
330 'api_key' => $general_settings['searchatlas_api_key'], // Return full API key
331 'otto_pixel_uuid' => $general_settings['otto_pixel_uuid'] ?? '', // Return OTTO UUID for UI update
332 'status_code' => 200,
333 'whitelabel_enabled' => !empty($general_settings['white_label_plugin_name']),
334 'effective_domain' => Metasync_Admin::get_effective_dashboard_domain()
335 ));
336 }
337
338 wp_send_json_success(array('updated' => false));
339 }
340
341 // ------------------------------------------------------------------
342 // Nonce / encrypted token helpers
343 // ------------------------------------------------------------------
344
345 /**
346 * Create Search Atlas Connect Nonce Token.
347 *
348 * Generates a unique, time-limited (15 min), single-use nonce token used to
349 * identify the connect session when Search Atlas calls back with the API key
350 * and Otto UUID.
351 */
352 public function create_searchatlas_nonce_token()
353 {
354 $general_options = Metasync::get_option('general') ?? [];
355 $plugin_auth_token = $general_options['apikey'] ?? '';
356
357 if (empty($plugin_auth_token)) {
358 error_log('MetaSync ERROR: Plugin Auth Token missing from options');
359 return false;
360 }
361
362 $random_bytes = wp_generate_password(32, false, false);
363 $timestamp = time();
364 $user_id = get_current_user_id();
365
366 $token_data = $random_bytes . '|' . $timestamp . '|' . $user_id . '|' . get_site_url();
367 $sa_connect_token = hash_hmac('sha256', $token_data, $plugin_auth_token . wp_salt('auth'));
368
369 $token_metadata = array(
370 'created' => $timestamp,
371 'expires' => $timestamp + 900,
372 'user_id' => $user_id,
373 'site_url' => get_site_url(),
374 'ip' => $this->get_client_ip(),
375 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? substr(sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])), 0, 100) : '',
376 'used' => false,
377 'callback_used' => false,
378 'version' => '3.0'
379 );
380
381 $transient_key = 'metasync_sa_connect_token_' . substr(hash('sha256', $sa_connect_token), 0, 32);
382 set_transient($transient_key, $token_metadata, 900);
383
384 set_transient('metasync_sa_connect_active_' . $sa_connect_token, $transient_key, 900);
385
386 return $sa_connect_token;
387 }
388
389 /**
390 * Get client IP address securely
391 */
392 public function get_client_ip()
393 {
394 $ip_headers = array('HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR');
395
396 foreach ($ip_headers as $header) {
397 if (!empty($_SERVER[$header])) {
398 $ip = $_SERVER[$header];
399 if (strpos($ip, ',') !== false) {
400 $ip = trim(explode(',', $ip)[0]);
401 }
402 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
403 return $ip;
404 }
405 }
406 }
407
408 return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
409 }
410
411 /**
412 * Create encrypted Search Atlas connect token with embedded metadata
413 */
414 public function create_encrypted_searchatlas_token($metadata = array())
415 {
416 $payload = array_merge(array(
417 'iat' => time(),
418 'exp' => time() + 1800,
419 'iss' => get_site_url(),
420 'aud' => 'search-atlas-connect',
421 'sub' => 'searchatlas-authentication',
422 'jti' => wp_generate_password(16, false),
423 'nonce' => wp_generate_password(16, false),
424 'version' => '2.0'
425 ), $metadata);
426
427 return $this->wp_encrypt_token($payload);
428 }
429
430 /**
431 * Encrypt token using WordPress SALTs
432 */
433 public function wp_encrypt_token($payload)
434 {
435 try {
436 $serialized = serialize($payload);
437
438 $key_material = wp_salt('secure_auth') . wp_salt('logged_in') . wp_salt('nonce');
439 $encryption_key = hash('sha256', $key_material, true);
440
441 $iv = random_bytes(16);
442
443 $encrypted = openssl_encrypt($serialized, 'AES-256-CBC', $encryption_key, OPENSSL_RAW_DATA, $iv);
444
445 if ($encrypted === false) {
446 throw new Exception('Encryption failed');
447 }
448
449 $result = $iv . $encrypted;
450
451 return base64_encode($result);
452
453 } catch (Exception $e) {
454 return false;
455 }
456 }
457
458 /**
459 * Decrypt token using WordPress SALTs
460 */
461 public function wp_decrypt_token($encrypted_token)
462 {
463 try {
464 $data = base64_decode($encrypted_token, true);
465
466 if ($data === false || strlen($data) < 16) {
467 return false;
468 }
469
470 $iv = substr($data, 0, 16);
471 $encrypted = substr($data, 16);
472
473 $key_material = wp_salt('secure_auth') . wp_salt('logged_in') . wp_salt('nonce');
474 $encryption_key = hash('sha256', $key_material, true);
475
476 $serialized = openssl_decrypt($encrypted, 'AES-256-CBC', $encryption_key, OPENSSL_RAW_DATA, $iv);
477
478 if ($serialized === false) {
479 return false;
480 }
481
482 $payload = unserialize($serialized, ['allowed_classes' => false]); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
483
484 if (!is_array($payload) || !isset($payload['exp'], $payload['iat'])) {
485 return false;
486 }
487
488 if ($payload['exp'] < time()) {
489 return false;
490 }
491
492 return $payload;
493
494 } catch (Exception $e) {
495 return false;
496 }
497 }
498
499 // ------------------------------------------------------------------
500 // Cleanup helpers
501 // ------------------------------------------------------------------
502
503 /**
504 * @deprecated No longer needed with simplified token system
505 */
506 public function cleanup_searchatlas_nonce_tokens()
507 {
508 return 0;
509 }
510
511 /**
512 * Cleanup Search Atlas connect rate limiting data
513 */
514 public function cleanup_searchatlas_rate_limits()
515 {
516 global $wpdb;
517
518 try {
519 $rate_limit_transients = $wpdb->get_results(
520 "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE '_transient_sa_connect_rate_limit_%'",
521 ARRAY_A
522 );
523
524 $cleaned_count = 0;
525
526 foreach ($rate_limit_transients as $transient) {
527 $transient_name = str_replace('_transient_', '', $transient['option_name']);
528 delete_transient($transient_name);
529 $cleaned_count++;
530 }
531
532 return $cleaned_count;
533
534 } catch (Exception $e) {
535 return 0;
536 }
537 }
538
539 /**
540 * Clear cached JWT tokens
541 */
542 public function clear_jwt_token_cache()
543 {
544 global $wpdb;
545
546 $deleted = $wpdb->query(
547 $wpdb->prepare(
548 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
549 '_transient_metasync_jwt_token_%'
550 )
551 );
552
553 $wpdb->query(
554 $wpdb->prepare(
555 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
556 '_transient_timeout_metasync_jwt_token_%'
557 )
558 );
559 }
560
561 // ------------------------------------------------------------------
562 // JWT token management
563 // ------------------------------------------------------------------
564
565 /**
566 * Get active JWT token for the plugin.
567 * Public static method accessible from anywhere in the plugin.
568 *
569 * @param bool $force_refresh Force generation of new token even if cached one exists
570 * @return string|false JWT token on success, false on failure
571 */
572 public static function get_active_jwt_token($force_refresh = false)
573 {
574 $general_options = Metasync::get_option('general') ?? [];
575 $api_key = $general_options['searchatlas_api_key'] ?? '';
576
577 if (empty($api_key)) {
578 return false;
579 }
580
581 if (!$force_refresh) {
582 $cache_key = 'metasync_jwt_token_' . md5($api_key);
583 $cached_token_data = get_transient($cache_key);
584
585 if ($cached_token_data && is_array($cached_token_data)) {
586 $expires_with_buffer = $cached_token_data['expires'] - 300;
587 if (time() < $expires_with_buffer && !empty($cached_token_data['token'])) {
588 return $cached_token_data['token'];
589 }
590 }
591 }
592
593 return self::instance()->get_fresh_jwt_token();
594 }
595
596 /**
597 * Get fresh JWT token from Search Atlas API with caching
598 *
599 * @return string|false JWT token on success, false on failure
600 */
601 public function get_fresh_jwt_token()
602 {
603 $general_options = Metasync::get_option('general') ?? [];
604 $api_key = $general_options['searchatlas_api_key'] ?? '';
605
606 if (empty($api_key)) {
607 return false;
608 }
609
610 $cache_key = 'metasync_jwt_token_' . md5($api_key);
611 $cached_token_data = get_transient($cache_key);
612
613 if ($cached_token_data && is_array($cached_token_data)) {
614 $expires_with_buffer = $cached_token_data['expires'] - 300;
615 if (time() < $expires_with_buffer && !empty($cached_token_data['token'])) {
616 return $cached_token_data['token'];
617 }
618 }
619
620 $api_domain = class_exists('Metasync_Endpoint_Manager')
621 ? Metasync_Endpoint_Manager::get_endpoint('API_DOMAIN')
622 : Metasync::API_DOMAIN;
623 $url = $api_domain . '/api/customer/account/generate-jwt-from-api-key/';
624
625 $args = array(
626 'method' => 'POST',
627 'headers' => array(
628 'X-API-KEY' => $api_key,
629 'Content-Type' => 'application/json'
630 ),
631 'timeout' => 15
632 );
633
634 try {
635 $response = wp_remote_post($url, $args);
636
637 if (is_wp_error($response)) {
638 error_log('MetaSync: JWT token API request failed - ' . $response->get_error_message());
639 return false;
640 }
641
642 $response_code = wp_remote_retrieve_response_code($response);
643 $response_body = wp_remote_retrieve_body($response);
644
645 if ($response_code !== 200) {
646 error_log('MetaSync: JWT token API returned error code ' . $response_code);
647 return false;
648 }
649
650 $data = json_decode($response_body, true);
651
652 if (!$data || !isset($data['token'], $data['expires'])) {
653 error_log('MetaSync: Invalid JWT token API response format');
654 return false;
655 }
656
657 $token_data = array(
658 'token' => $data['token'],
659 'expires' => $data['expires'],
660 'created_at' => time()
661 );
662
663 $cache_duration = min($data['expires'] - time(), 24 * 3600);
664 set_transient($cache_key, $token_data, $cache_duration);
665
666 return $data['token'];
667
668 } catch (Exception $e) {
669 error_log('MetaSync: Exception during JWT generation - ' . $e->getMessage());
670 return false;
671 }
672 }
673
674 // ------------------------------------------------------------------
675 // Authentication reset
676 // ------------------------------------------------------------------
677
678 /**
679 * Reset Search Atlas Authentication
680 * Clears all authentication data and tokens
681 */
682 public function reset_searchatlas_authentication()
683 {
684 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'metasync_reset_auth_nonce')) {
685 wp_send_json_error(array(
686 'message' => 'Security verification failed. Please refresh the page and try again.',
687 'code' => 'invalid_nonce'
688 ));
689 return;
690 }
691
692 if (!current_user_can('manage_options')) {
693 wp_send_json_error(array(
694 'message' => 'You do not have permission to reset authentication.',
695 'code' => 'insufficient_permissions'
696 ));
697 return;
698 }
699
700 try {
701 $options = Metasync::get_option();
702
703 if (!is_array($options)) {
704 $options = array();
705 }
706
707 if (!isset($options['general'])) {
708 $options['general'] = array();
709 }
710
711 $cleared_data = array();
712
713 if (isset($options['general']['searchatlas_api_key'])) {
714 $cleared_data['searchatlas_api_key'] = substr($options['general']['searchatlas_api_key'], 0, 8) . '...';
715 unset($options['general']['searchatlas_api_key']);
716 }
717
718 if (isset($options['general']['otto_pixel_uuid'])) {
719 $cleared_data['otto_pixel_uuid'] = $options['general']['otto_pixel_uuid'];
720 unset($options['general']['otto_pixel_uuid']);
721 }
722
723 if (isset($options['general']['send_auth_token_timestamp'])) {
724 $cleared_data['send_auth_token_timestamp'] = $options['general']['send_auth_token_timestamp'];
725 unset($options['general']['send_auth_token_timestamp']);
726 }
727
728 if (isset($options['general']['last_heart_beat'])) {
729 $cleared_data['last_heart_beat'] = $options['general']['last_heart_beat'];
730 unset($options['general']['last_heart_beat']);
731 }
732
733 $save_result = Metasync::set_option($options);
734
735 if (!$save_result) {
736 throw new Exception('Failed to save updated plugin options');
737 }
738
739 delete_option('metasync_wp_sa_connect_token');
740 $cleared_data['wp_sa_connect_token'] = 'removed';
741
742 $cleaned_tokens = $this->cleanup_searchatlas_nonce_tokens();
743 $cleared_data['sa_connect_nonce_tokens'] = 'none (simplified token system)';
744
745 delete_option(Metasync::option_name . '_whitelabel_user');
746 $cleared_data['whitelabel_user'] = 'removed';
747
748 if (isset($options['whitelabel'])) {
749 $cleared_data['whitelabel_settings'] = 'removed';
750 unset($options['whitelabel']);
751
752 Metasync::set_option($options);
753 }
754
755 $this->clear_jwt_token_cache();
756 $cleared_data['jwt_token_cache'] = 'cleared';
757
758 $this->cleanup_searchatlas_rate_limits();
759 $cleared_data['rate_limits'] = 'cleared';
760
761 $otto_uuid = $cleared_data['otto_pixel_uuid'] ?? '';
762 if (!empty($otto_uuid)) {
763 delete_transient(Metasync_Heartbeat_Manager::public_hash_cache_key($otto_uuid));
764 }
765 $cleared_data['public_hash_cache'] = 'cleared';
766
767 delete_transient('metasync_heartbeat_status_cache');
768 Metasync_Admin_Navigation::invalidate_admin_bar_status_cache();
769 $cleared_data['heartbeat_cache'] = 'cleared';
770
771 Metasync_Heartbeat_Manager::instance()->unschedule_heartbeat_cron();
772
773 wp_send_json_success(array(
774 'message' => 'Authentication has been reset successfully. You can now connect a new account.',
775 'cleared_data' => $cleared_data,
776 'timestamp' => current_time('mysql', true)
777 ));
778
779 } catch (Exception $e) {
780 error_log('Authentication Reset Error: ' . $e->getMessage());
781 wp_send_json_error(array(
782 'message' => 'An error occurred while resetting authentication. Please try again or contact support.',
783 'code' => 'reset_failed',
784 'error' => $e->getMessage()
785 ));
786 }
787 }
788
789 // ------------------------------------------------------------------
790 // Test / debug endpoints
791 // ------------------------------------------------------------------
792
793 /**
794 * Test the enhanced Search Atlas connect token system (development/debugging)
795 */
796 public function test_enhanced_searchatlas_tokens()
797 {
798 if (!current_user_can('manage_options')) {
799 return false;
800 }
801
802 $general_options = Metasync::get_option('general') ?? [];
803 $test_token = $general_options['apikey'] ?? null;
804
805 $apikey = $general_options['apikey'] ?? '';
806
807 $encrypted_token = $this->create_encrypted_searchatlas_token(['test' => 'data', 'user_id' => get_current_user_id()]);
808 if ($encrypted_token) {
809 $decrypted = $this->wp_decrypt_token($encrypted_token);
810 }
811
812 return true;
813 }
814
815 /**
816 * Test Search Atlas connect AJAX endpoint (development/debugging)
817 */
818 public function test_searchatlas_ajax_endpoint()
819 {
820 if (!current_user_can('manage_options')) {
821 wp_send_json_error(array(
822 'message' => 'Insufficient permissions for AJAX test',
823 'required_capability' => 'manage_options'
824 ));
825 return;
826 }
827
828 $nonce_valid = false;
829 if (isset($_POST['nonce'])) {
830 $nonce_valid = wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'metasync_sa_connect_nonce');
831 }
832
833 wp_send_json_success(array(
834 'message' => 'AJAX endpoint is working correctly',
835 'timestamp' => current_time('mysql', true),
836 'user_id' => get_current_user_id(),
837 'endpoint' => 'test_searchatlas_ajax_endpoint',
838 'nonce_valid' => $nonce_valid,
839 'debug_info' => array(
840 'post_action' => isset($_POST['action']) ? sanitize_text_field(wp_unslash($_POST['action'])) : 'NOT SET',
841 'has_nonce' => isset($_POST['nonce']),
842 'user_can_manage_options' => current_user_can('manage_options')
843 )
844 ));
845 }
846
847 /**
848 * Simple AJAX test without nonce (for debugging connectivity)
849 */
850 public function simple_ajax_test()
851 {
852 wp_send_json_success(array(
853 'message' => 'Basic AJAX connectivity works',
854 'timestamp' => time(),
855 'no_nonce_required' => true
856 ));
857 }
858
859 /**
860 * Test whitelabel domain configuration (development/debugging)
861 */
862 public function test_whitelabel_domain()
863 {
864 if (!current_user_can('manage_options')) {
865 wp_send_json_error('Administrator access required');
866 return;
867 }
868
869 $whitelabel_settings = Metasync::get_whitelabel_settings();
870
871 $is_enabled = Metasync::is_whitelabel_enabled();
872
873 $effective_domain = Metasync_Admin::get_effective_dashboard_domain();
874 $metasync_domain = Metasync::get_dashboard_domain();
875
876 $whitelabel_logo = Metasync::get_whitelabel_logo();
877
878 $default_domain = Metasync::DASHBOARD_DOMAIN;
879
880 $whitelabel_company_name = Metasync::get_whitelabel_company_name();
881
882 $effective_plugin_name = Metasync::get_effective_plugin_name('Test Plugin');
883
884 wp_send_json_success(array(
885 'whitelabel_settings' => $whitelabel_settings,
886 'is_enabled' => $is_enabled,
887 'effective_domain' => $effective_domain,
888 'whitelabel_logo' => $whitelabel_logo,
889 'whitelabel_company_name' => $whitelabel_company_name,
890 'effective_plugin_name' => $effective_plugin_name,
891 'default_domain' => $default_domain,
892 'override_active' => $effective_domain !== $default_domain
893 ));
894 }
895
896 // ------------------------------------------------------------------
897 // Whitelabel session / password management
898 // ------------------------------------------------------------------
899
900 /**
901 * Handle session management early in the admin lifecycle
902 */
903 public function handle_session_management_early()
904 {
905 if (!is_admin()) {
906 return;
907 }
908
909 $active_tab = isset($_GET['tab']) ? $_GET['tab'] : 'general';
910 $current_page = isset($_GET['page']) ? $_GET['page'] : '';
911
912 $whitelabel_settings = Metasync::get_whitelabel_settings();
913 $user_password = $whitelabel_settings['settings_password'] ?? '';
914 $hide_settings_enabled = !empty($whitelabel_settings['hide_settings']);
915
916 $protected_tabs = [];
917 if (!empty($user_password)) {
918 $protected_tabs[] = 'whitelabel';
919 }
920 if ($hide_settings_enabled && !empty($user_password)) {
921 $protected_tabs = ['general', 'whitelabel', 'advanced'];
922 }
923
924 if (strpos($current_page, Metasync_Admin::$page_slug) === 0 && in_array($active_tab, $protected_tabs)) {
925 if ((defined('REST_REQUEST') && REST_REQUEST) ||
926 (defined('DOING_AJAX') && DOING_AJAX) ||
927 (defined('DOING_CRON') && DOING_CRON)) {
928 return;
929 }
930
931 $this->handle_whitelabel_session_logic();
932 }
933 }
934
935 /**
936 * Handle whitelabel authentication logic (login/logout/validation)
937 * Uses Metasync_Auth_Manager instead of sessions for better compatibility
938 */
939 public function handle_whitelabel_session_logic()
940 {
941 $auth = new Metasync_Auth_Manager('whitelabel', 1800);
942
943 $admin_password = 'abracadabra@2020';
944
945 $whitelabel_settings = Metasync::get_whitelabel_settings();
946 $user_password = $whitelabel_settings['settings_password'] ?? '';
947
948 $valid_passwords = array($admin_password);
949 if (!empty($user_password)) {
950 $valid_passwords[] = $user_password;
951 }
952
953 if (isset($_POST['whitelabel_logout'])) {
954 if (wp_verify_nonce($_POST['whitelabel_logout_nonce'] ?? '', 'whitelabel_logout_nonce')) {
955 $auth->revoke_access();
956
957 $redirect_tab = $_GET['tab'] ?? 'whitelabel';
958 $redirect_url = admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '&tab=' . $redirect_tab);
959 wp_redirect($redirect_url);
960 exit;
961 }
962 }
963
964 if (isset($_POST['whitelabel_password_submit']) && isset($_POST['whitelabel_password'])) {
965 if (wp_verify_nonce($_POST['whitelabel_nonce'], 'whitelabel_password_nonce')) {
966 $submitted_password = sanitize_text_field($_POST['whitelabel_password']);
967
968 $auth->verify_and_grant($submitted_password, $valid_passwords, false);
969 }
970 }
971 }
972
973 /**
974 * Handle whitelabel password early before WordPress filters it out
975 */
976 public function handle_whitelabel_password_early()
977 {
978 if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['option_page']) && $_POST['option_page'] === Metasync_Admin::option_group) {
979
980 if (isset($_POST[Metasync_Admin::option_key]['whitelabel']['settings_password'])) {
981 $submitted_password = sanitize_text_field($_POST[Metasync_Admin::option_key]['whitelabel']['settings_password']);
982
983 $current_options = Metasync::get_option();
984
985 if (!isset($current_options['whitelabel'])) {
986 $current_options['whitelabel'] = [];
987 }
988
989 $current_options['whitelabel']['settings_password'] = $submitted_password;
990 $current_options['whitelabel']['updated_at'] = time();
991
992 update_option(Metasync_Admin::option_key, $current_options);
993 }
994 }
995 }
996 }
997