'akkedli_api_key', 'webhook_url' => 'akkedli_webhook_url', 'api_key_validated' => 'akkedli_api_key_validated', 'connected_at' => 'akkedli_connected_at', 'store_domain' => 'akkedli_store_domain', ); public static function exists() { $missing = new stdClass(); return get_option(self::OPTION, $missing) !== $missing; } public static function status() { $state = self::load(); return null === $state ? 'absent' : (is_array($state) ? $state['mode'] : 'corrupt'); } public static function legacy_outbound_allowed() { return in_array(self::status(), array('absent', 'migration_pending', 'v2_pending', 'v2_failed'), true); } public static function load() { $missing = new stdClass(); $envelope = get_option(self::OPTION, $missing); if ($envelope === $missing) { return null; } try { $state = Akkedli_V2_Crypto::decrypt($envelope); } catch (Throwable $error) { unset($error, $envelope); return false; } unset($envelope); if (!self::is_valid($state)) { unset($state); return false; } return $state; } public static function replace(array $state, $expected_state = null) { if (!self::is_valid($state)) { return false; } $missing = new stdClass(); $previous = get_option(self::OPTION, $missing); if (null !== $expected_state && !Akkedli_V2_Binding_State::matches_envelope($previous, $missing, $expected_state)) { return false; } try { $envelope = Akkedli_V2_Crypto::encrypt($state); } catch (Throwable $error) { unset($error, $previous); return false; } $written = $previous === $missing ? add_option(self::OPTION, $envelope, '', 'no') : self::compare_and_replace($previous, $envelope); $stored = get_option(self::OPTION, ''); $verified = $written && is_string($stored) && hash_equals($envelope, $stored); if ($verified) { $round_trip = self::load(); $verified = is_array($round_trip) && hash_equals( wp_json_encode($state, JSON_UNESCAPED_SLASHES), wp_json_encode($round_trip, JSON_UNESCAPED_SLASHES) ); unset($round_trip); } if (!$verified) { if ($previous === $missing) { self::compare_and_delete($envelope); } else { self::compare_and_replace($envelope, $previous); } } unset($state, $previous, $envelope, $stored); return $verified; } public static function delete_pending() { $missing = new stdClass(); $envelope = get_option(self::OPTION, $missing); if ($envelope === $missing) { return true; } $state = self::load(); if (!is_array($state) || 'migration_pending' !== $state['mode']) { return false; } unset($state); return self::compare_and_delete($envelope); } public static function create_legacy_backup() { $backup = array(); foreach (self::$legacy_options as $field => $option) { $missing = new stdClass(); $value = get_option($option, $missing); $present = $value !== $missing; if ('api_key_validated' === $field) { $value = $present ? (bool) $value : false; } else { $value = $present && is_scalar($value) ? (string) $value : ''; } $backup[$field] = array( 'present' => $present, 'value' => $value, 'autoload' => $present && self::is_option_autoloaded($option), ); } return $backup; } public static function clear_legacy_connection() { foreach (self::$legacy_options as $option) { delete_option($option); } foreach (self::$legacy_options as $option) { $missing = new stdClass(); if (get_option($option, $missing) !== $missing) { return false; } } return true; } public static function restore_legacy_connection(array $backup) { if (!self::is_valid_backup($backup)) { return false; } foreach (self::$legacy_options as $field => $option) { delete_option($option); if ($backup[$field]['present']) { add_option($option, $backup[$field]['value'], '', $backup[$field]['autoload'] ? 'yes' : 'no'); } } foreach (self::$legacy_options as $field => $option) { $missing = new stdClass(); $actual = get_option($option, $missing); $valid = !$backup[$field]['present'] ? $actual === $missing : ('api_key_validated' === $field ? (bool) $actual === $backup[$field]['value'] : is_scalar($actual) && (string) $actual === $backup[$field]['value']); $valid = $valid && (!$backup[$field]['present'] || self::is_option_autoloaded($option) === $backup[$field]['autoload']); if (!$valid) { self::clear_legacy_connection(); return false; } } return true; } public static function get_legacy_callback_key() { $state = self::load(); if (!is_array($state)) { return ''; } if ('v2' === $state['mode'] && isset($state['legacy_callback_key'])) { return $state['legacy_callback_key']; } if (in_array($state['mode'], array('migration_pending', 'v2_pending', 'v2_failed'), true)) { $entry = $state['legacy_backup']['api_key']; return $entry['present'] ? $entry['value'] : ''; } return ''; } public static function is_valid($state) { if (!is_array($state) || !isset($state['schema_version'], $state['mode']) || self::SCHEMA_VERSION !== $state['schema_version']) { return false; } if ('v2_pending' === $state['mode'] || 'v2_failed' === $state['mode']) { return Akkedli_V2_Binding_State::valid($state); } if ('migration_pending' === $state['mode']) { return self::has_exact_keys($state, array('schema_version', 'mode', 'legacy_backup')) && self::is_valid_backup($state['legacy_backup']); } if ('disconnected' === $state['mode']) { return self::has_exact_keys($state, array('schema_version', 'mode')); } if ('v2' !== $state['mode']) { return false; } $keys = array('schema_version', 'mode', 'credential_id', 'secret', 'protocol_version'); if (isset($state['legacy_callback_key'])) $keys[] = 'legacy_callback_key'; if (isset($state['outbox_recovery_pending'])) $keys[] = 'outbox_recovery_pending'; if (!self::has_exact_keys($state, $keys) || !is_string($state['credential_id']) || !preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/Di', $state['credential_id']) || 1 !== $state['protocol_version'] || !is_string($state['secret']) || (isset($state['outbox_recovery_pending']) && true !== $state['outbox_recovery_pending'])) { return false; } try { $secret = Akkedli_V2_Crypto::base64url_decode($state['secret']); } catch (Throwable $error) { unset($error); return false; } $valid = strlen($secret) === 32; unset($secret); return $valid && (!isset($state['legacy_callback_key']) || (is_string($state['legacy_callback_key']) && $state['legacy_callback_key'] !== '')); } public static function is_valid_backup($backup) { if (!is_array($backup) || !self::has_exact_keys($backup, array_keys(self::$legacy_options))) { return false; } foreach (self::$legacy_options as $field => $option) { unset($option); $entry = $backup[$field]; if (!is_array($entry) || !self::has_exact_keys($entry, array('present', 'value', 'autoload')) || !is_bool($entry['present']) || !is_bool($entry['autoload'])) { return false; } if ('api_key_validated' === $field ? !is_bool($entry['value']) : !is_string($entry['value'])) { return false; } } return true; } private static function has_exact_keys(array $value, array $keys) { $actual = array_keys($value); sort($actual); sort($keys); return $actual === $keys; } private static function is_option_autoloaded($option) { global $wpdb; $autoload = $wpdb->get_var($wpdb->prepare( "SELECT autoload FROM {$wpdb->options} WHERE option_name = %s", $option )); return in_array($autoload, array('yes', 'on', 'auto-on', 'auto'), true); } private static function clear_option_cache() { wp_cache_delete(self::OPTION, 'options'); wp_cache_delete('alloptions', 'options'); wp_cache_delete('notoptions', 'options'); } private static function compare_and_replace($expected, $replacement) { global $wpdb; $updated = $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->options} SET option_value = %s, autoload = %s WHERE option_name = %s AND option_value = %s", maybe_serialize($replacement), 'no', self::OPTION, maybe_serialize($expected) ) ); self::clear_option_cache(); return 1 === $updated; } private static function compare_and_delete($expected) { global $wpdb; $deleted = $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name = %s AND option_value = %s", self::OPTION, maybe_serialize($expected) ) ); self::clear_option_cache(); return 1 === $deleted; } }