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

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

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