false, 'sms_provider' => '']; } /** * 获取当前 provider adapter */ public function get_provider(): ?ProviderInterface { $config = $this->get_provider_config(); $key = sanitize_key((string) ($config['sms_provider'] ?? '')); return SMSProviderFactory::get($key); } /** * 发送短信验证码 * * @param string $phone 手机号 * @return array{success: bool, message: string, remaining_time?: int} */ public function send_code(string $phone): array { $config = $this->get_provider_config(); if (empty($config['sms_enabled'])) { return ['success' => false, 'message' => __('短信服务未启用', 'aksrt')]; } $provider_key = sanitize_key((string) ($config['sms_provider'] ?? '')); if ($provider_key === '') { return ['success' => false, 'message' => __('未配置短信供应商', 'aksrt')]; } $provider = SMSProviderFactory::get($provider_key); if ($provider === null) { return ['success' => false, 'message' => sprintf(__('短信供应商 "%s" 不可用', 'aksrt'), $provider_key)]; } // 校验配置完整性 $missing = $provider->validate_config($config); if (!empty($missing)) { return ['success' => false, 'message' => sprintf(__('短信配置不完整,缺少: %s', 'aksrt'), implode(', ', $missing))]; } // 频率限制:间隔限制(对齐 legacy 60 秒冷却 + 返回剩余时间) $rate_limit = max(10, (int) ($config['sms_rate_limit'] ?? 60)); $rate_key = 'aksrt_sms_rate_' . md5($phone); $rate_set = get_transient($rate_key); if ($rate_set !== false) { // 计算剩余等待时间 $rate_ts_key = 'aksrt_sms_rate_ts_' . md5($phone); $sent_time = (int) get_transient($rate_ts_key); $elapsed = $sent_time > 0 ? time() - $sent_time : $rate_limit; $remaining = max(1, $rate_limit - $elapsed); return [ 'success' => false, 'message' => sprintf(__('请 %d 秒后再试', 'aksrt'), $remaining), 'remaining_time' => $remaining, ]; } // 每日限制 $daily_limit = max(1, (int) ($config['sms_daily_limit'] ?? 10)); $daily_key = 'aksrt_sms_daily_' . md5($phone) . '_' . date('Ymd'); $daily_count = (int) get_transient($daily_key); if ($daily_count >= $daily_limit) { return ['success' => false, 'message' => __('今日发送次数已达上限', 'aksrt')]; } $code_length = max(4, min(8, (int) ($config['sms_code_length'] ?? 6))); $code = $this->generate_code($code_length); $ttl = max(60, min(600, (int) ($config['sms_code_expire'] ?? 300))); // 存储验证码(绑定手机号) set_transient('aksrt_sms_code_' . md5($phone), $code, $ttl); set_transient('aksrt_sms_phone_' . md5($phone), $phone, $ttl); set_transient($rate_key, '1', $rate_limit); set_transient('aksrt_sms_rate_ts_' . md5($phone), time(), $rate_limit); set_transient($daily_key, $daily_count + 1, DAY_IN_SECONDS); // 委托 provider 发送 $result = $provider->send_code($phone, $code, $config); if (!empty($result['success'])) { return [ 'success' => true, 'message' => $result['message'] ?? __('验证码已发送', 'aksrt'), 'remaining_time' => $rate_limit, ]; } // 发送失败:删除已存储的验证码,避免无效等待 delete_transient('aksrt_sms_code_' . md5($phone)); delete_transient('aksrt_sms_phone_' . md5($phone)); return ['success' => false, 'message' => $result['message'] ?? __('发送失败,请稍后重试', 'aksrt')]; } /** * 验证短信验证码(对齐 legacy:同时校验验证码和目标手机号) * * @param string $phone 手机号 * @param string $code 验证码 * @return array{success: bool, message: string} */ public function verify_code(string $phone, string $code): array { // 错误频率限制检查 $error_check = $this->check_error_frequency('sms_verify_' . md5($phone)); if (!$error_check['allowed']) { return [ 'success' => false, 'message' => sprintf(__('验证失败次数过多,请 %d 秒后再试', 'aksrt'), $error_check['remaining']), 'remaining_time' => $error_check['remaining'], ]; } $code_key = 'aksrt_sms_code_' . md5($phone); $phone_key = 'aksrt_sms_phone_' . md5($phone); $stored_code = get_transient($code_key); $stored_phone = get_transient($phone_key); // 验证码不存在或已过期 if ($stored_code === false) { $this->record_error('sms_verify_' . md5($phone)); return ['success' => false, 'message' => __('验证码已过期或不存在', 'aksrt')]; } // 验证码不匹配 if (!hash_equals((string) $stored_code, $code)) { $this->record_error('sms_verify_' . md5($phone)); return ['success' => false, 'message' => __('验证码不正确', 'aksrt')]; } // 目标手机号绑定校验 if ($stored_phone !== false && (string) $stored_phone !== $phone) { $this->record_error('sms_verify_' . md5($phone)); return ['success' => false, 'message' => __('手机号不匹配', 'aksrt')]; } // 验证成功:清除验证码和错误计数 delete_transient($code_key); delete_transient($phone_key); $this->reset_error_frequency('sms_verify_' . md5($phone)); return ['success' => true, 'message' => __('验证成功', 'aksrt')]; } /** * 兼容旧接口:verify_code 返回 bool */ public function verify_code_bool(string $phone, string $code): bool { $result = $this->verify_code($phone, $code); return !empty($result['success']); } /** * 生成验证码 */ private function generate_code(int $length = 6): string { $max = (int) str_pad('9', $length, '9'); return str_pad((string) random_int(0, $max), $length, '0', STR_PAD_LEFT); } /* ------------------------------------------------------------------ * 错误频率限制(对齐 legacy 的 error_frequency_limit) * ----------------------------------------------------------------*/ /** * 检查是否被频率限制 * * @return array{allowed: bool, remaining: int} */ private function check_error_frequency(string $key): array { $count_key = 'aksrt_sms_err_' . $key; $time_key = 'aksrt_sms_err_ts_' . $key; $count = (int) get_transient($count_key); $last = (int) get_transient($time_key); if ($count <= 0) { return ['allowed' => true, 'remaining' => 0]; } // 超过 10 次错误:3 分钟冷却 if ($count >= 10) { $cooldown = 180; $elapsed = time() - $last; if ($elapsed < $cooldown) { return ['allowed' => false, 'remaining' => $cooldown - $elapsed]; } // 冷却结束,重置 $this->reset_error_frequency($key); return ['allowed' => true, 'remaining' => 0]; } // 超过 5 次错误:30 秒冷却 if ($count >= 5) { $cooldown = 30; $elapsed = time() - $last; if ($elapsed < $cooldown) { return ['allowed' => false, 'remaining' => $cooldown - $elapsed]; } } return ['allowed' => true, 'remaining' => 0]; } /** * 记录一次错误 */ private function record_error(string $key): void { $count_key = 'aksrt_sms_err_' . $key; $time_key = 'aksrt_sms_err_ts_' . $key; $count = (int) get_transient($count_key); set_transient($count_key, $count + 1, HOUR_IN_SECONDS); set_transient($time_key, time(), HOUR_IN_SECONDS); } /** * 重置错误计数 */ private function reset_error_frequency(string $key): void { delete_transient('aksrt_sms_err_' . $key); delete_transient('aksrt_sms_err_ts_' . $key); } /* ------------------------------------------------------------------ * AJAX Handlers * ----------------------------------------------------------------*/ /** * 手机号格式校验(对齐 legacy:支持中国号和国际号) */ private function validate_phone(string $phone): bool { $config = $this->get_provider_config(); $limit_ch = !isset($config['sms_number_limit_ch']) || !empty($config['sms_number_limit_ch']); if ($limit_ch) { // 仅中国大陆手机号 return (bool) preg_match('/^1[3-9]\d{9}$/', $phone); } // 国际手机号:5 位以上数字,可选 + 前缀 return (bool) preg_match('/^(?:\d{5,}|\+\d{6,})$/', $phone); } /** * AJAX 发送验证码 */ public function ajax_send_code(): void { check_ajax_referer('aksrt_sms', '_nonce'); $phone = sanitize_text_field((string) ($_POST['phone'] ?? '')); if (!$this->validate_phone($phone)) { wp_send_json_error(['message' => __('手机号格式不正确', 'aksrt')]); } $scene = sanitize_key((string) ($_POST['scene'] ?? 'register')); if (!array_key_exists($scene, function_exists('aksrt_captcha_scenes') ? aksrt_captcha_scenes() : [])) { $scene = 'register'; } if (function_exists('aksrt_require_captcha')) { $captcha_result = aksrt_require_captcha($scene, wp_unslash($_POST)); if ($captcha_result !== true) { wp_send_json_error(['message' => $captcha_result ?: __('人机验证失败', 'aksrt')]); } } $result = $this->send_code($phone); if ($result['success']) { wp_send_json_success([ 'message' => $result['message'], 'remaining_time' => $result['remaining_time'] ?? 60, ]); } $error_data = ['message' => $result['message']]; if (isset($result['remaining_time'])) { $error_data['remaining_time'] = $result['remaining_time']; } wp_send_json_error($error_data); } /** * AJAX: Test SMS provider (admin only). */ public function ajax_test_provider(): void { if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('权限不足', 'aksrt')]); } $phone = sanitize_text_field((string) ($_POST['phone'] ?? '')); if (!preg_match('/^1[3-9]\d{9}$/', $phone)) { wp_send_json_error(['message' => __('手机号格式不正确', 'aksrt')]); } $result = $this->test_provider($phone); wp_send_json($result); } /** * AJAX: Get SMS provider status (admin only). */ public function ajax_provider_status(): void { if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('权限不足', 'aksrt')]); } wp_send_json_success($this->get_provider_status()); } /** * 获取 provider 真实运行时状态 */ public function get_provider_status(): array { $config = $this->get_provider_config(); $provider_key = sanitize_key((string) ($config['sms_provider'] ?? '')); $status = [ 'enabled' => !empty($config['sms_enabled']), 'provider' => $provider_key ?: 'none', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => [], 'code_length' => max(4, min(8, (int) ($config['sms_code_length'] ?? 6))), 'code_expire' => max(60, min(600, (int) ($config['sms_code_expire'] ?? 300))), 'rate_limit' => max(10, (int) ($config['sms_rate_limit'] ?? 60)), 'daily_limit' => max(1, (int) ($config['sms_daily_limit'] ?? 10)), 'notify_bind' => !empty($config['sms_notify_bind']), 'notify_login' => !empty($config['sms_notify_login']), ]; $provider = SMSProviderFactory::get($provider_key); if ($provider !== null) { $provider_status = $provider->get_status($config); $status['configured'] = !empty($provider_status['configured']); $status['available'] = !empty($provider_status['available']); $status['verified'] = !empty($provider_status['verified']); $status['transport'] = (string) ($provider_status['transport'] ?? ''); $status['missing_fields'] = (array) ($provider_status['missing_fields'] ?? []); } return $status; } /** * Test provider connectivity (admin only). */ public function test_provider(string $phone): array { $config = $this->get_provider_config(); if (empty($config['sms_enabled'])) { return ['success' => false, 'message' => __('短信服务未启用', 'aksrt')]; } $provider_key = sanitize_key((string) ($config['sms_provider'] ?? '')); if ($provider_key === '') { return ['success' => false, 'message' => __('未配置短信供应商', 'aksrt')]; } $provider = SMSProviderFactory::get($provider_key); if ($provider === null) { return ['success' => false, 'message' => sprintf(__('短信供应商 "%s" 不可用', 'aksrt'), $provider_key)]; } $missing = $provider->validate_config($config); if (!empty($missing)) { return ['success' => false, 'message' => sprintf(__('缺少配置项: %s', 'aksrt'), implode(', ', $missing))]; } return $this->send_code($phone); } }