优化了一些问题
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Auth;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
final class OAuthTransactionStore {
|
||||
|
||||
private const PREFIX = 'aksrt_oauth_tx_';
|
||||
private const COOKIE = 'aksrt_oauth_browser';
|
||||
private const LOCK_TTL = 30;
|
||||
|
||||
public static function browser_id(): string {
|
||||
$value = isset($_COOKIE[self::COOKIE]) ? sanitize_text_field(wp_unslash((string) $_COOKIE[self::COOKIE])) : '';
|
||||
if (preg_match('/^[A-Za-z0-9_-]{32,80}$/', $value) === 1) {
|
||||
return $value;
|
||||
}
|
||||
$value = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
|
||||
if (!headers_sent()) {
|
||||
setcookie(self::COOKIE, $value, [
|
||||
'expires' => time() + MONTH_IN_SECONDS,
|
||||
'path' => defined('COOKIEPATH') && COOKIEPATH ? COOKIEPATH : '/',
|
||||
'domain' => defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : '',
|
||||
'secure' => is_ssl(),
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
}
|
||||
$_COOKIE[self::COOKIE] = $value;
|
||||
return $value;
|
||||
}
|
||||
|
||||
public static function issue(string $kind, array $payload, int $ttl = 600, bool $bind_browser = true): string {
|
||||
$token = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
|
||||
$key = self::key($token);
|
||||
$row = [
|
||||
'kind' => sanitize_key($kind),
|
||||
'payload' => $payload,
|
||||
'browser_hash' => $bind_browser ? hash('sha256', self::browser_id()) : '',
|
||||
'created_at' => time(),
|
||||
'expires_at' => time() + max(30, $ttl),
|
||||
'claimed_at' => 0,
|
||||
];
|
||||
if (!add_option($key, $row, '', false)) {
|
||||
throw new \RuntimeException('oauth_transaction_issue_failed');
|
||||
}
|
||||
return $token;
|
||||
}
|
||||
|
||||
public static function peek(string $token, string $kind = '', bool $require_browser = true): ?array {
|
||||
$key = self::key($token);
|
||||
$row = get_option($key, null);
|
||||
if (is_array($row) && (int) ($row['expires_at'] ?? 0) < time()) {
|
||||
delete_option($key);
|
||||
return null;
|
||||
}
|
||||
if (!self::valid_row($row, $kind, $require_browser)) {
|
||||
return null;
|
||||
}
|
||||
return $row;
|
||||
}
|
||||
|
||||
public static function claim(string $token, string $kind = '', bool $require_browser = true): ?array {
|
||||
global $wpdb;
|
||||
$key = self::key($token);
|
||||
$row = get_option($key, null);
|
||||
if (is_array($row) && (int) ($row['expires_at'] ?? 0) < time()) {
|
||||
delete_option($key);
|
||||
return null;
|
||||
}
|
||||
if (!self::valid_row($row, $kind, $require_browser)) {
|
||||
return null;
|
||||
}
|
||||
$serialized = maybe_serialize($row);
|
||||
$row['claimed_at'] = time();
|
||||
$deleted = $wpdb->query($wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->options} WHERE option_name = %s AND option_value = %s",
|
||||
$key,
|
||||
$serialized
|
||||
));
|
||||
if ($deleted !== 1) {
|
||||
return null;
|
||||
}
|
||||
wp_cache_delete($key, 'options');
|
||||
return $row;
|
||||
}
|
||||
|
||||
public static function update(string $token, string $kind, callable $mutator, bool $require_browser = false): bool {
|
||||
global $wpdb;
|
||||
$key = self::key($token);
|
||||
$current = get_option($key, null);
|
||||
if (is_array($current) && (int) ($current['expires_at'] ?? 0) < time()) {
|
||||
delete_option($key);
|
||||
return false;
|
||||
}
|
||||
if (!self::valid_row($current, $kind, $require_browser)) {
|
||||
return false;
|
||||
}
|
||||
$next = $mutator($current);
|
||||
if (!is_array($next)) {
|
||||
return false;
|
||||
}
|
||||
$updated = $wpdb->query($wpdb->prepare(
|
||||
"UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s AND option_value = %s",
|
||||
maybe_serialize($next),
|
||||
$key,
|
||||
maybe_serialize($current)
|
||||
));
|
||||
if ($updated === 1) {
|
||||
wp_cache_delete($key, 'options');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function delete(string $token): void {
|
||||
delete_option(self::key($token));
|
||||
}
|
||||
|
||||
public static function acquire_lock(string $scope, int $ttl = self::LOCK_TTL) {
|
||||
$key = self::lock_key($scope);
|
||||
$token = wp_generate_uuid4();
|
||||
$value = ['token' => $token, 'expires_at' => time() + max(5, $ttl)];
|
||||
if (add_option($key, $value, '', false)) {
|
||||
return $token;
|
||||
}
|
||||
$current = get_option($key, null);
|
||||
if (!is_array($current) || (int) ($current['expires_at'] ?? 0) >= time()) {
|
||||
return false;
|
||||
}
|
||||
global $wpdb;
|
||||
$updated = $wpdb->query($wpdb->prepare(
|
||||
"UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s AND option_value = %s",
|
||||
maybe_serialize($value),
|
||||
$key,
|
||||
maybe_serialize($current)
|
||||
));
|
||||
if ($updated === 1) {
|
||||
wp_cache_delete($key, 'options');
|
||||
return $token;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function release_lock(string $scope, string $token): void {
|
||||
global $wpdb;
|
||||
$key = self::lock_key($scope);
|
||||
$current = get_option($key, null);
|
||||
if (!is_array($current) || !hash_equals((string) ($current['token'] ?? ''), $token)) {
|
||||
return;
|
||||
}
|
||||
$deleted = $wpdb->query($wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->options} WHERE option_name = %s AND option_value = %s",
|
||||
$key,
|
||||
maybe_serialize($current)
|
||||
));
|
||||
if ($deleted === 1) {
|
||||
wp_cache_delete($key, 'options');
|
||||
}
|
||||
}
|
||||
|
||||
public static function cleanup(int $limit = 200): int {
|
||||
global $wpdb;
|
||||
$rows = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE %s LIMIT %d",
|
||||
$wpdb->esc_like(self::PREFIX) . '%',
|
||||
max(1, min(1000, $limit))
|
||||
), ARRAY_A);
|
||||
$removed = 0;
|
||||
foreach (is_array($rows) ? $rows : [] as $row) {
|
||||
$value = maybe_unserialize($row['option_value'] ?? '');
|
||||
if (!is_array($value) || (int) ($value['expires_at'] ?? 0) >= time()) {
|
||||
continue;
|
||||
}
|
||||
if (delete_option((string) $row['option_name'])) {
|
||||
$removed++;
|
||||
}
|
||||
}
|
||||
return $removed;
|
||||
}
|
||||
|
||||
private static function valid_row($row, string $kind, bool $require_browser): bool {
|
||||
if (!is_array($row) || (int) ($row['expires_at'] ?? 0) < time()) {
|
||||
return false;
|
||||
}
|
||||
if ($kind !== '' && !hash_equals(sanitize_key($kind), (string) ($row['kind'] ?? ''))) {
|
||||
return false;
|
||||
}
|
||||
if ($require_browser) {
|
||||
$expected = (string) ($row['browser_hash'] ?? '');
|
||||
if ($expected === '' || !hash_equals($expected, hash('sha256', self::browser_id()))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function key(string $token): string {
|
||||
return self::PREFIX . hash('sha256', trim($token));
|
||||
}
|
||||
|
||||
private static function lock_key(string $scope): string {
|
||||
return self::PREFIX . 'lock_' . hash('sha256', $scope);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user