Files
wordpress-theme-aksrt/tests/widget-system/captcha-security-harness.php
T
kate522andClaude Opus 5 35bb96e82f refactor: rename the theme brand and every identifier from AKSRT to Jio
Full-project rename. `git grep -i aksrt` is now empty apart from the
DBMigrator 3.3.0 changelog note that documents the rename itself.

Code identifiers
- PHP namespace AKSRT\ -> Jio\ (composer PSR-4 + 378 declarations)
- global constants AKSRT_* -> JIO_* (VERSION/DIR/URI, PAY_TYPE_*, CAPTCHA_*)
- procedural functions, hooks, options, meta keys, table names,
  cookies, query vars, ajax actions and nonces: aksrt_ -> jio_
- text domain aksrt -> jio; Theme Name -> Jio
- JS globals: aksrtData/aksrtAdminData/... -> jioData/jioAdminData/...,
  window.AKSRTCaptcha & friends -> window.JioCaptcha, Aksrt* -> Jio*
- Vue components Aks*.vue -> Jio*.vue (15 files renamed + imports)
- CSS classes/data-attrs/custom properties: aksrt- -> jio-
- REST namespace aksrt/v1 -> jio/v1 (incl. payment gateway callbacks)

Storage layer
- DB tables {prefix}aksrt_* -> {prefix}jio_*; SCHEMA_VERSION 3.2.0 -> 3.3.0
  so dbDelta creates the new names on install
- widget id_base aksrt_widget_* -> jio_widget_*
- uninstall.php drop list rebuilt from the actual migrator tables
  (dropped 3 stale entries, added the 17 missing ones)

Removed
- hardcoded https://api.aksrt.com/version.json default update endpoint;
  get_endpoint() now reads the 「更新端点」 setting and do_check() no-ops
  when it is empty

Files renamed: inc/helpers/aksrt-*.php -> jio-*.php (54),
assets/admin/src/components/form/Aks*.vue -> Jio*.vue (15).
Build artifacts regenerated: assets/css/build, assets/js/runtime,
assets/admin/build.

Verified: audit-psr4-paths errors=0, audit-admin-contracts errors=0
(warnings=56, unchanged baseline), 813/813 widget-system tests,
44/44 phpunit, php -l clean across all PHP.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-01 20:05:04 +08:00

186 lines
9.1 KiB
PHP

<?php
/**
* Executable captcha configuration and fail-closed contract harness.
*/
declare(strict_types=1);
namespace {
define('ABSPATH', __DIR__ . '/');
define('MINUTE_IN_SECONDS', 60);
define('MONTH_IN_SECONDS', 2592000);
define('ARRAY_A', 'ARRAY_A');
$GLOBALS['jio_test_options'] = [];
$GLOBALS['wp_cache'] = [];
final class HarnessWpdb {
public string $options = 'wp_options';
public function prepare(string $query, ...$args): array { return [$query, $args]; }
public function esc_like(string $value): string { return $value; }
public function get_results($prepared, $format): array { return []; }
public function query($prepared): int {
[, $args] = $prepared;
[$key, $serialized] = $args;
if (!array_key_exists($key, $GLOBALS['jio_test_options']) || serialize($GLOBALS['jio_test_options'][$key]) !== $serialized) return 0;
unset($GLOBALS['jio_test_options'][$key]);
return 1;
}
}
$GLOBALS['wpdb'] = new HarnessWpdb();
final class WP_Error {
public function __construct(private string $code, private string $message, private array $data = []) {}
public function get_error_code(): string { return $this->code; }
public function get_error_message(): string { return $this->message; }
}
function is_wp_error($value): bool { return $value instanceof WP_Error; }
function __(string $text, string $domain = ''): string { return $text; }
function sanitize_key($value): string { return preg_replace('/[^a-z0-9_\-]/', '', strtolower((string) $value)) ?? ''; }
function sanitize_text_field($value): string { return trim(strip_tags((string) $value)); }
function sanitize_textarea_field($value): string { return sanitize_text_field($value); }
function wp_kses_post($value): string { return strip_tags((string) $value, '<b><strong><em><i><a><p><br>'); }
function esc_url_raw($value): string { return trim((string) $value); }
function wp_unslash($value) { return $value; }
function get_option(string $key, $default = false) { return array_key_exists($key, $GLOBALS['jio_test_options']) ? $GLOBALS['jio_test_options'][$key] : $default; }
function update_option(string $key, $value): bool { $GLOBALS['jio_test_options'][$key] = $value; return true; }
function add_option(string $key, $value, string $deprecated = '', bool $autoload = true): bool {
if (array_key_exists($key, $GLOBALS['jio_test_options'])) return false;
$GLOBALS['jio_test_options'][$key] = $value;
return true;
}
function delete_option(string $key): bool {
if (!array_key_exists($key, $GLOBALS['jio_test_options'])) return false;
unset($GLOBALS['jio_test_options'][$key]);
return true;
}
function maybe_serialize($value): string { return serialize($value); }
function maybe_unserialize($value) { $decoded = @unserialize((string) $value); return $decoded === false && $value !== serialize(false) ? $value : $decoded; }
function wp_cache_delete(string $key, string $group = ''): void { unset($GLOBALS['wp_cache'][$group][$key]); }
function wp_salt(string $scheme = 'auth'): string { return 'captcha-harness-' . $scheme; }
function is_ssl(): bool { return true; }
function wp_rand(int $min, int $max): int { return $max; }
function add_action(...$args): void {}
function add_filter(...$args): void {}
function current_user_can(string $capability): bool { return false; }
function get_transient(string $key) { return false; }
function set_transient(string $key, $value, int $ttl): bool { return true; }
function get_current_user_id(): int { return 1; }
function wp_parse_url(string $url, int $component = -1) { return parse_url($url, $component); }
function home_url(string $path = ''): string { return 'https://example.test/' . ltrim($path, '/'); }
function wp_json_encode($value): string { return json_encode($value, JSON_UNESCAPED_SLASHES); }
function wp_generate_uuid4(): string { return '00000000-0000-4000-8000-000000000000'; }
function wp_remote_post(string $url, array $args = []) { return new WP_Error('unexpected_http', 'unexpected HTTP request'); }
function wp_remote_retrieve_response_code($response): int { return 500; }
function wp_remote_retrieve_body($response): string { return ''; }
function esc_html($value): string { return (string) $value; }
function wp_unique_id(string $prefix = ''): string { return $prefix . '1'; }
}
namespace Jio\Settings {
final class Settings {
public static function flush_settings_cache(): void {}
public static function sync_native_site_icon(string $value): void {}
}
}
namespace {
require dirname(__DIR__, 2) . '/inc/modules/captcha.php';
require dirname(__DIR__, 2) . '/src/Attachment/UploadLimits.php';
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaPermissions.php';
require dirname(__DIR__, 2) . '/src/Core/Color.php';
require dirname(__DIR__, 2) . '/src/Admin/SchemaProviderOAuthValidation.php';
require dirname(__DIR__, 2) . '/src/Admin/SchemaProviderOptionMatch.php';
require dirname(__DIR__, 2) . '/src/Admin/SchemaProvider.php';
function check(bool $condition, string $message): void {
if (!$condition) {
throw new RuntimeException($message);
}
}
check(jio_captcha_default_config()['enabled_scenarios'] === [], 'captcha scenarios are not opt-in');
check(!in_array('domain', array_keys(jio_captcha_provider_defaults('cloudflare')), true), 'Cloudflare domain remains allowed');
$GLOBALS['jio_test_options']['jio_captcha_config'] = [
'enabled' => true,
'active_provider' => 'cloudflare',
'enabled_scenarios' => ['login'],
'cloudflare' => [
'site_key' => 'site-key',
'secret_key' => 'stored-secret',
'theme' => 'dark',
'size' => 'compact',
'domain' => 'should-be-dropped.test',
],
];
jio_captcha_config_reset();
$provider = \Jio\Admin\SchemaProvider::get_instance();
$read = $provider->read_settings('permissions', 'login');
check(($read['cloudflare']['secret_key'] ?? '') === '***', 'nested secret was not masked');
check(!isset($read['cloudflare']['domain']), 'unknown provider key was exposed');
$provider->write_settings('permissions', [
'cloudflare' => [
'site_key' => 'new-site-key',
'secret_key' => '***',
'theme' => 'light',
'size' => 'normal',
'domain' => 'ignored.test',
],
], 'login');
$stored = $GLOBALS['jio_test_options']['jio_captcha_config']['cloudflare'];
check($stored['secret_key'] === 'stored-secret', 'masked secret did not preserve stored value');
check($stored['site_key'] === 'new-site-key', 'public provider field was not updated');
check(!isset($stored['domain']), 'unknown provider key was persisted');
$provider->write_settings('permissions', [
'cloudflare' => ['secret_key' => ''],
], 'login');
check($GLOBALS['jio_test_options']['jio_captcha_config']['cloudflare']['secret_key'] === 'stored-secret', 'empty secret did not preserve stored value');
$provider->write_settings('permissions', [
'__clear_secrets' => ['cloudflare.secret_key'],
], 'login');
check($GLOBALS['jio_test_options']['jio_captcha_config']['cloudflare']['secret_key'] === '', 'explicit nested secret clear failed');
$GLOBALS['jio_test_options']['jio_captcha_config'] = [
'enabled' => true,
'active_provider' => '',
'enabled_scenarios' => ['login'],
];
jio_captcha_config_reset();
$failed_closed = jio_require_captcha('login', []);
check(is_wp_error($failed_closed), 'enabled scene with missing provider failed open');
check($failed_closed->get_error_code() === 'jio_captcha_config', 'fail-closed error code mismatch');
$GLOBALS['jio_test_options']['jio_captcha_config'] = [
'enabled' => true,
'active_provider' => 'cloudflare',
'enabled_scenarios' => ['login', 'comment'],
'cloudflare' => ['site_key' => 'site', 'secret_key' => 'secret', 'theme' => 'auto', 'size' => 'normal'],
];
jio_captcha_config_reset();
$_COOKIE[JIO_CAPTCHA_BROWSER_COOKIE] = str_repeat('A', 43);
$challenge = jio_captcha_issue_challenge('login', 'cloudflare');
check(jio_captcha_validate_challenge($challenge, 'login', 'cloudflare', false), 'valid scene challenge failed');
check(!jio_captcha_validate_challenge($challenge, 'comment', 'cloudflare', false), 'cross-scene challenge accepted');
check(!jio_captcha_validate_challenge($challenge, 'login', 'geetest', false), 'cross-provider challenge accepted');
$_COOKIE[JIO_CAPTCHA_BROWSER_COOKIE] = str_repeat('B', 43);
check(!jio_captcha_validate_challenge($challenge, 'login', 'cloudflare', false), 'cross-browser challenge accepted');
$_COOKIE[JIO_CAPTCHA_BROWSER_COOKIE] = str_repeat('A', 43);
check(jio_captcha_validate_challenge($challenge, 'login', 'cloudflare', true), 'valid challenge consume failed');
check(!jio_captcha_validate_challenge($challenge, 'login', 'cloudflare', true), 'challenge replay accepted');
$proof = jio_captcha_issue_proof('login', 'cloudflare');
check(!jio_captcha_consume_proof($proof, 'comment', 'cloudflare'), 'cross-scene proof accepted');
check(jio_captcha_consume_proof($proof, 'login', 'cloudflare'), 'valid proof consume failed');
check(!jio_captcha_consume_proof($proof, 'login', 'cloudflare'), 'proof replay accepted');
$headers = jio_tencent_tc3_sign('id', 'key', 'captcha', 1700000000, '2023-11-14', 'DescribeCaptchaResult', '2019-07-22', '{}');
check(str_starts_with($headers['Authorization'], 'TC3-HMAC-SHA256 Credential=id/2023-11-14/captcha/tc3_request'), 'TC3 authorization scope mismatch');
echo "Captcha security harness passed.\n";
}