PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.1
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.1
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
← All changes | includes/class-metasync-connect-manager.php +189 -63 2.6.21 → 2.7.1 View file →
@@ -88,9 +88,9 @@
88 88 $general_options = Metasync::get_option('general') ?? [];
89 89 $plugin_auth_token = $general_options['apikey'] ?? '';
90 90
91 91 if (empty($plugin_auth_token)) {
92 - error_log('MetaSync ERROR: Plugin Auth Token missing from options - should have been generated during activation');
92 + error_log('MetaSync ERROR: Plugin Auth Token missing from options - should have been generated during activation'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- unreachable in production
93 93 return false;
94 94 }
95 95
96 96 return $plugin_auth_token;
@@ -188,9 +188,9 @@
188 188 wp_send_json_error(array('message' => 'Failed to save new token'));
189 189 }
190 190
191 191 } catch (Exception $e) {
192 - error_log('Plugin Auth Token Refresh Error: ' . $e->getMessage());
192 + error_log('Plugin Auth Token Refresh Error: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
193 193 wp_send_json_error(array('message' => 'Error generating new token'));
194 194 }
195 195 }
196 196
@@ -222,9 +222,9 @@
222 222 wp_send_json_error(array('message' => 'Plugin Auth Token not found'));
223 223 }
224 224
225 225 } catch (Exception $e) {
226 - error_log('Get Plugin Auth Token Error: ' . $e->getMessage());
226 + error_log('Get Plugin Auth Token Error: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
227 227 wp_send_json_error(array('message' => 'Error retrieving Plugin Auth Token'));
228 228 }
229 229 }
230 230
@@ -270,15 +270,21 @@
270 270 $domain = str_replace('://www.', '://', get_site_url());
271 271
272 272 $dashboard_domain = Metasync_Admin::get_effective_dashboard_domain();
273 273
274 - $sa_connect_url = $dashboard_domain . '/sso/wordpress?' . http_build_query([
274 + $connect_args = [
275 275 'nonce_token' => $sa_connect_token,
276 276 'domain' => $domain,
277 277 'callback_url' => get_rest_url(null, 'metasync/v1/searchatlas/connect/callback'),
278 278 'return_url' => admin_url('admin.php?page=' . Metasync_Admin::$page_slug)
279 - ]);
279 + ];
280 280
281 + if (Metasync_Headless_Config::is_active()) {
282 + $connect_args['frontend_domain'] = Metasync_Headless_Config::get_frontend_domain();
283 + }
284 +
285 + $sa_connect_url = $dashboard_domain . '/sso/wordpress?' . http_build_query($connect_args);
286 +
281 287 wp_send_json_success(array(
282 288 'connect_url' => $sa_connect_url,
283 289 'nonce_token' => $sa_connect_token,
284 290 'debug_info' => array(
@@ -323,9 +329,9 @@
323 329 if (!$this_auth_completed && wp_using_ext_object_cache()) {
324 330 global $wpdb;
325 331
326 332 // Check expiry first
327 - $timeout = $wpdb->get_var(
333 + $timeout = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- manual transient rows on the options table (autoload=no) — mirrors core transient storage; TTL reads need the paired timeout row
328 334 $wpdb->prepare(
329 335 "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
330 336 '_transient_timeout_' . $success_key
331 337 )
@@ -332,12 +338,12 @@
332 338 );
333 339
334 340 if ($timeout && (int) $timeout < time()) {
335 341 // 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));
342 + $wpdb->delete($wpdb->options, array('option_name' => '_transient_' . $success_key)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- manual transient rows on the options table (autoload=no) — mirrors core transient storage; TTL reads need the paired timeout row
343 + $wpdb->delete($wpdb->options, array('option_name' => '_transient_timeout_' . $success_key)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- manual transient rows on the options table (autoload=no) — mirrors core transient storage; TTL reads need the paired timeout row
338 344 } else {
339 - $db_value = $wpdb->get_var(
345 + $db_value = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- manual transient rows on the options table (autoload=no) — mirrors core transient storage; TTL reads need the paired timeout row
340 346 $wpdb->prepare(
341 347 "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
342 348 '_transient_' . $success_key
343 349 )
@@ -353,10 +359,10 @@
353 359 delete_transient($success_key);
354 360 // Also clean up DB fallback rows
355 361 if (wp_using_ext_object_cache()) {
356 362 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));
363 + $wpdb->delete($wpdb->options, array('option_name' => '_transient_' . $success_key)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- manual transient rows on the options table (autoload=no) — mirrors core transient storage; TTL reads need the paired timeout row
364 + $wpdb->delete($wpdb->options, array('option_name' => '_transient_timeout_' . $success_key)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- manual transient rows on the options table (autoload=no) — mirrors core transient storage; TTL reads need the paired timeout row
359 365 }
360 366
361 367 // Get current settings to return connection status
362 368 $general_settings = Metasync::get_option('general') ?? [];
@@ -557,9 +563,9 @@
557 563 $general_options = Metasync::get_option('general') ?? [];
558 564 $plugin_auth_token = $general_options['apikey'] ?? '';
559 565
560 566 if (empty($plugin_auth_token)) {
561 - error_log('MetaSync ERROR: Plugin Auth Token missing from options');
567 + error_log('MetaSync ERROR: Plugin Auth Token missing from options'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
562 568 return false;
563 569 }
564 570
565 571 $random_bytes = wp_generate_password(32, false, false);
@@ -597,12 +603,12 @@
597 603 $timeout_active = '_transient_timeout_metasync_sa_connect_active_' . $sa_connect_token;
598 604 $timeout_metadata = '_transient_timeout_' . $transient_key;
599 605 $expires = time() + 900;
600 606
601 - $wpdb->replace($wpdb->options, array('option_name' => $active_option, 'option_value' => $transient_key, 'autoload' => 'no'));
602 - $wpdb->replace($wpdb->options, array('option_name' => $timeout_active, 'option_value' => $expires, 'autoload' => 'no'));
603 - $wpdb->replace($wpdb->options, array('option_name' => $metadata_option, 'option_value' => maybe_serialize($token_metadata), 'autoload' => 'no'));
604 - $wpdb->replace($wpdb->options, array('option_name' => $timeout_metadata, 'option_value' => $expires, 'autoload' => 'no'));
607 + $wpdb->replace($wpdb->options, array('option_name' => $active_option, 'option_value' => $transient_key, 'autoload' => 'no')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- manual transient rows on the options table (autoload=no) — mirrors core transient storage; TTL reads need the paired timeout row
608 + $wpdb->replace($wpdb->options, array('option_name' => $timeout_active, 'option_value' => $expires, 'autoload' => 'no')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- manual transient rows on the options table (autoload=no) — mirrors core transient storage; TTL reads need the paired timeout row
609 + $wpdb->replace($wpdb->options, array('option_name' => $metadata_option, 'option_value' => maybe_serialize($token_metadata), 'autoload' => 'no')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- manual transient rows on the options table (autoload=no) — mirrors core transient storage; TTL reads need the paired timeout row
610 + $wpdb->replace($wpdb->options, array('option_name' => $timeout_metadata, 'option_value' => $expires, 'autoload' => 'no')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- manual transient rows on the options table (autoload=no) — mirrors core transient storage; TTL reads need the paired timeout row
605 611 }
606 612
607 613 return $sa_connect_token;
608 614 }
@@ -736,9 +742,9 @@
736 742 {
737 743 global $wpdb;
738 744
739 745 try {
740 - $rate_limit_transients = $wpdb->get_results(
746 + $rate_limit_transients = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- transient enumeration/cleanup on the options table — no API to scan or bulk-delete transient rows
741 747 "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE '_transient_sa_connect_rate_limit_%'",
742 748 ARRAY_A
743 749 );
744 750
@@ -763,9 +769,9 @@
763 769 public function clear_jwt_token_cache()
764 770 {
765 771 global $wpdb;
766 772
767 - $deleted = $wpdb->query(
773 + $deleted = $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- manual transient rows on the options table (autoload=no) — mirrors core transient storage; TTL reads need the paired timeout row
768 774 $wpdb->prepare(
769 775 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
770 776 '_transient_metasync_jwt_token_%'
771 777 )
@@ -770,9 +776,9 @@
770 776 '_transient_metasync_jwt_token_%'
771 777 )
772 778 );
773 779
774 - $wpdb->query(
780 + $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- manual transient rows on the options table (autoload=no) — mirrors core transient storage; TTL reads need the paired timeout row
775 781 $wpdb->prepare(
776 782 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
777 783 '_transient_timeout_metasync_jwt_token_%'
778 784 )
@@ -816,8 +822,45 @@
816 822 return self::instance()->get_fresh_jwt_token();
817 823 }
818 824
819 825 /**
826 + * Rotate the MCP signing secret so all previously issued tokens are invalid.
827 + */
828 + public function revoke_mcp_jwt_tokens()
829 + {
830 + delete_option('metasync_jwt_secret');
831 + wp_cache_delete('metasync_jwt_secret', 'options');
832 + }
833 +
834 + /**
835 + * Return a valid cached JWT without making a network request.
836 + *
837 + * Non-blocking telemetry must never refresh credentials as a side effect.
838 + *
839 + * @return string|false Cached JWT token, or false when none is available.
840 + */
841 + public static function get_cached_jwt_token()
842 + {
843 + $api_key = Metasync::get_searchatlas_api_key();
844 + if ($api_key === false || $api_key === '') {
845 + return false;
846 + }
847 +
848 + $cache_key = 'metasync_jwt_token_' . md5($api_key);
849 + $cached_token_data = get_transient($cache_key);
850 + if (!is_array($cached_token_data) || empty($cached_token_data['token'])) {
851 + return false;
852 + }
853 +
854 + $expires = isset($cached_token_data['expires']) ? (int) $cached_token_data['expires'] : 0;
855 + if ($expires <= time() + 300) {
856 + return false;
857 + }
858 +
859 + return (string) $cached_token_data['token'];
860 + }
861 +
862 + /**
820 863 * Get fresh JWT token from Search Atlas API with caching
821 864 *
822 865 * @return string|false JWT token on success, false on failure
823 866 */
@@ -859,9 +902,9 @@
859 902 try {
860 903 $response = wp_remote_post($url, $args);
861 904
862 905 if (is_wp_error($response)) {
863 - error_log('MetaSync: JWT token API request failed - ' . $response->get_error_message());
906 + error_log('MetaSync: JWT token API request failed - ' . $response->get_error_message()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
864 907 return false;
865 908 }
866 909
867 910 $response_code = wp_remote_retrieve_response_code($response);
@@ -867,9 +910,9 @@
867 910 $response_code = wp_remote_retrieve_response_code($response);
868 911 $response_body = wp_remote_retrieve_body($response);
869 912
870 913 if ($response_code !== 200) {
871 - error_log('MetaSync: JWT token API returned error code ' . $response_code);
914 + error_log('MetaSync: JWT token API returned error code ' . $response_code); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
872 915 return false;
873 916 }
874 917
875 918 $data = json_decode($response_body, true);
@@ -874,9 +917,9 @@
874 917
875 918 $data = json_decode($response_body, true);
876 919
877 920 if (!$data || !isset($data['token'], $data['expires'])) {
878 - error_log('MetaSync: Invalid JWT token API response format');
921 + error_log('MetaSync: Invalid JWT token API response format'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
879 922 return false;
880 923 }
881 924
882 925 $token_data = array(
@@ -890,9 +933,9 @@
890 933
891 934 return $data['token'];
892 935
893 936 } catch (Exception $e) {
894 - error_log('MetaSync: Exception during JWT generation - ' . $e->getMessage());
937 + error_log('MetaSync: Exception during JWT generation - ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
895 938 return false;
896 939 }
897 940 }
898 941
@@ -900,8 +943,56 @@
900 943 // Authentication reset
901 944 // ------------------------------------------------------------------
902 945
903 946 /**
947 + * Save the options blob so that removing a key actually removes it.
948 + *
949 + * Metasync::set_option() goes through update_option(), and WordPress pipes
950 + * every update_option() call for a registered setting through
951 + * sanitize_option() — which runs Metasync_Settings_Registration::sanitize(),
952 + * the callback register_setting() attached. That sanitizer is built for a
953 + * Settings-API form POST, where the submission carries only the fields of
954 + * one tab: it takes the *stored* option as its base layer and merges the
955 + * incoming array over it, deliberately so that keys a partial submission
956 + * omits survive rather than being wiped.
957 + *
958 + * Correct for a form. Fatal here. Disconnecting works by unsetting keys and
959 + * saving the result, and the sanitizer re-reads the pre-write option as its
960 + * base, so every key this handler removed came straight back — the API key
961 + * byte-identical to the one just cleared. The account looked connected again
962 + * on the next page load.
963 + *
964 + * So this write detaches that callback for its own duration and restores it
965 + * afterwards. Scoped to the disconnect handler rather than changed inside
966 + * set_option(): the merge is right for the form path, and the sanitizer also
967 + * carries the whitelabel password encryption and recovery-password guards
968 + * that other programmatic writes still want.
969 + *
970 + * @param array $options The complete options array to store verbatim.
971 + * @return bool Whether the write succeeded.
972 + */
973 + private function write_options_without_stored_merge($options)
974 + {
975 + $hook = 'sanitize_option_' . Metasync::option_name;
976 +
977 + // register_setting() attaches it at the default priority against the
978 + // registration singleton, so the same callable identity removes it.
979 + $callback = class_exists('Metasync_Settings_Registration')
980 + ? array(Metasync_Settings_Registration::instance(), 'sanitize')
981 + : null;
982 +
983 + $was_attached = ($callback !== null) && remove_filter($hook, $callback);
984 +
985 + $save_result = Metasync::set_option($options);
986 +
987 + if ($was_attached) {
988 + add_filter($hook, $callback);
989 + }
990 +
991 + return $save_result;
992 + }
993 +
994 + /**
904 995 * Reset Search Atlas Authentication
905 996 * Clears all authentication data and tokens
906 997 */
907 998 public function reset_searchatlas_authentication()
@@ -972,9 +1063,9 @@
972 1063 # throttle timestamps out of the main blob).
973 1064 delete_option(Metasync::heartbeat_throttle_option);
974 1065 $cleared_data['heartbeat_throttle'] = 'removed';
975 1066
976 - $save_result = Metasync::set_option($options);
1067 + $save_result = $this->write_options_without_stored_merge($options);
977 1068 Metasync::invalidate_api_key_cache();
978 1069
979 1070 if (!$save_result) {
980 1071 throw new Exception('Failed to save updated plugin options');
@@ -992,12 +1083,13 @@
992 1083 if (isset($options['whitelabel'])) {
993 1084 $cleared_data['whitelabel_settings'] = 'removed';
994 1085 unset($options['whitelabel']);
995 1086
996 - Metasync::set_option($options);
1087 + $this->write_options_without_stored_merge($options);
997 1088 }
998 1089
999 1090 $this->clear_jwt_token_cache();
1091 + $this->revoke_mcp_jwt_tokens();
1000 1092 $cleared_data['jwt_token_cache'] = 'cleared';
1001 1093
1002 1094 $this->cleanup_searchatlas_rate_limits();
1003 1095 $cleared_data['rate_limits'] = 'cleared';
@@ -1026,9 +1118,9 @@
1026 1118 'timestamp' => current_time('mysql', true)
1027 1119 ));
1028 1120
1029 1121 } catch (Exception $e) {
1030 - error_log('Authentication Reset Error: ' . $e->getMessage());
1122 + error_log('Authentication Reset Error: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
1031 1123 wp_send_json_error(array(
1032 1124 'message' => 'An error occurred while resetting authentication. Please try again or contact support.',
1033 1125 'code' => 'reset_failed',
1034 1126 'error' => $e->getMessage()
@@ -1094,20 +1186,8 @@
1094 1186 ));
1095 1187 }
1096 1188
1097 1189 /**
1098 - * Simple AJAX test without nonce (for debugging connectivity)
1099 - */
1100 - public function simple_ajax_test()
1101 - {
1102 - wp_send_json_success(array(
1103 - 'message' => 'Basic AJAX connectivity works',
1104 - 'timestamp' => time(),
1105 - 'no_nonce_required' => true
1106 - ));
1107 - }
1108 -
1109 - /**
1110 1190 * Test whitelabel domain configuration (development/debugging)
1111 1191 */
1112 1192 public function test_whitelabel_domain()
1113 1193 {
@@ -1155,10 +1235,10 @@
1155 1235 if (!is_admin()) {
1156 1236 return;
1157 1237 }
1158 1238
1159 - $active_tab = isset($_GET['tab']) ? $_GET['tab'] : 'general';
1160 - $current_page = isset($_GET['page']) ? $_GET['page'] : '';
1239 + $active_tab = isset($_GET['tab']) ? $_GET['tab'] : 'general'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing, each action verifies its own nonce
1240 + $current_page = isset($_GET['page']) ? $_GET['page'] : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing, each action verifies its own nonce
1161 1241
1162 1242 $whitelabel_settings = Metasync::get_whitelabel_settings();
1163 1243 $user_password = $whitelabel_settings['settings_password'] ?? '';
1164 1244 $hide_settings_enabled = !empty($whitelabel_settings['hide_settings']);
@@ -1189,25 +1269,15 @@
1189 1269 public function handle_whitelabel_session_logic()
1190 1270 {
1191 1271 $auth = new Metasync_Auth_Manager('whitelabel', 1800);
1192 1272
1193 - $admin_password = 'abracadabra@2020';
1194 -
1195 - // Decrypted plaintext for verification; '' when unset or undecryptable.
1196 - $user_password = Metasync::get_whitelabel_password();
1197 -
1198 - $valid_passwords = array($admin_password);
1199 - if (!empty($user_password)) {
1200 - $valid_passwords[] = $user_password;
1201 - }
1202 -
1203 1273 if (isset($_POST['whitelabel_logout'])) {
1204 1274 if (wp_verify_nonce($_POST['whitelabel_logout_nonce'] ?? '', 'whitelabel_logout_nonce')) {
1205 1275 $auth->revoke_access();
1206 1276
1207 - $redirect_tab = $_GET['tab'] ?? 'whitelabel';
1277 + $redirect_tab = isset($_GET['tab']) ? sanitize_key(wp_unslash($_GET['tab'])) : 'whitelabel';
1208 1278 $redirect_url = admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '&tab=' . $redirect_tab);
1209 - wp_redirect($redirect_url);
1279 + wp_safe_redirect($redirect_url);
1210 1280 exit;
1211 1281 }
1212 1282 }
1213 1283
@@ -1212,11 +1282,30 @@
1212 1282 }
1213 1283
1214 1284 if (isset($_POST['whitelabel_password_submit']) && isset($_POST['whitelabel_password'])) {
1215 1285 if (wp_verify_nonce($_POST['whitelabel_nonce'], 'whitelabel_password_nonce')) {
1216 - $submitted_password = sanitize_text_field($_POST['whitelabel_password']);
1286 + $submitted_password = (string) wp_unslash($_POST['whitelabel_password']);
1217 1287
1218 - $auth->verify_and_grant($submitted_password, $valid_passwords, false);
1288 + $lock_owner = '';
1289 + if (!Metasync_Admin_Ajax::instance()->acquire_recovery_lock($lock_owner)) {
1290 + return;
1291 + }
1292 + $shutdown_lock_owner = $lock_owner;
1293 + register_shutdown_function(function () use ($shutdown_lock_owner) {
1294 + Metasync_Admin_Ajax::instance()->release_recovery_lock($shutdown_lock_owner);
1295 + });
1296 +
1297 + try {
1298 + // This request may have cached settings before a reset
1299 + // completed. Refresh after taking the shared lock, then
1300 + // verify and grant against one consistent password state.
1301 + Metasync_Admin_Ajax::instance()->refresh_recovery_state_cache();
1302 + if (function_exists('wp_cache_delete')) wp_cache_delete('metasync_auth_revoke_epoch_whitelabel', 'options');
1303 + $auth->verify_and_grant($submitted_password, $this->get_whitelabel_valid_passwords(), false);
1304 + } finally {
1305 + Metasync_Admin_Ajax::instance()->release_recovery_lock($lock_owner);
1306 + $lock_owner = '';
1307 + }
1219 1308 }
1220 1309 }
1221 1310 }
1222 1311
@@ -1224,23 +1313,60 @@
1224 1313 * Handle whitelabel password early before WordPress filters it out
1225 1314 */
1226 1315 public function handle_whitelabel_password_early()
1227 1316 {
1228 - if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['option_page']) && $_POST['option_page'] === Metasync_Admin::option_group) {
1317 + if (!current_user_can('manage_options')) {
1318 + return;
1319 + }
1229 1320
1321 + if (
1322 + !isset($_POST['meta_sync_nonce'])
1323 + || !wp_verify_nonce(wp_unslash($_POST['meta_sync_nonce']), 'meta_sync_general_setting_nonce')
1324 + ) {
1325 + return;
1326 + }
1327 +
1328 + if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST' && isset($_POST['option_page']) && $_POST['option_page'] === Metasync_Admin::option_group) {
1329 +
1230 1330 if (isset($_POST[Metasync_Admin::option_key]['whitelabel']['settings_password'])) {
1231 - $submitted_password = sanitize_text_field($_POST[Metasync_Admin::option_key]['whitelabel']['settings_password']);
1331 + $submitted_password = (string) wp_unslash($_POST[Metasync_Admin::option_key]['whitelabel']['settings_password']);
1332 + $lock_owner = '';
1333 + if (!Metasync_Admin_Ajax::instance()->acquire_recovery_lock($lock_owner)) {
1334 + return;
1335 + }
1336 + $shutdown_lock_owner = $lock_owner;
1337 + register_shutdown_function(function () use ($shutdown_lock_owner) {
1338 + Metasync_Admin_Ajax::instance()->release_recovery_lock($shutdown_lock_owner);
1339 + });
1232 1340
1233 - $current_options = Metasync::get_option();
1341 + try {
1342 + Metasync_Admin_Ajax::instance()->refresh_recovery_state_cache();
1343 + $current_options = Metasync::get_option();
1234 1344
1235 - if (!isset($current_options['whitelabel'])) {
1236 - $current_options['whitelabel'] = [];
1237 - }
1345 + if (!isset($current_options['whitelabel'])) {
1346 + $current_options['whitelabel'] = [];
1347 + }
1238 1348
1239 - $current_options['whitelabel']['settings_password'] = Metasync::encrypt_secret($submitted_password);
1240 - $current_options['whitelabel']['updated_at'] = time();
1349 + $current_options['whitelabel']['settings_password'] = Metasync::encrypt_secret($submitted_password);
1350 + $current_options['whitelabel']['updated_at'] = time();
1241 1351
1242 - update_option(Metasync_Admin::option_key, $current_options);
1352 + update_option(Metasync_Admin::option_key, $current_options);
1353 + } finally {
1354 + Metasync_Admin_Ajax::instance()->release_recovery_lock($lock_owner);
1355 + $lock_owner = '';
1356 + }
1243 1357 }
1244 1358 }
1359 + }
1360 +
1361 + /**
1362 + * Return the site-specific passwords accepted by the whitelabel gate.
1363 + *
1364 + * @return array<int, string>
1365 + */
1366 + private function get_whitelabel_valid_passwords()
1367 + {
1368 + $user_password = Metasync::get_whitelabel_password();
1369 +
1370 + return $user_password === '' ? array() : array($user_password);
1245 1371 }
1246 1372 }