refactor: flatten src/AKSRT → src (drop theme-identifier dir)
Move all domain folders from src/AKSRT/{Domain} to src/{Domain}; remap
Composer PSR-4 "AKSRT\\": "src/"; update audit script + README + ADR
paths; remove dead tools/build-psr4-map.php (scanned deleted inc/classes).
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Sms;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
interface ProviderInterface {
|
||||
|
||||
/**
|
||||
* 供应商唯一标识
|
||||
*/
|
||||
public function get_key(): string;
|
||||
|
||||
/**
|
||||
* 供应商显示名称
|
||||
*/
|
||||
public function get_label(): string;
|
||||
|
||||
/**
|
||||
* 校验配置完整性,返回缺失字段列表
|
||||
*
|
||||
* @param array $config SMS 配置
|
||||
* @return array 缺失的字段名列表,空数组表示配置完整
|
||||
*/
|
||||
public function validate_config(array $config): array;
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
*
|
||||
* @param string $phone 手机号
|
||||
* @param string $code 验证码
|
||||
* @param array $config SMS 配置
|
||||
* @return array{success: bool, message: string, transport?: string}
|
||||
*/
|
||||
public function send_code(string $phone, string $code, array $config): array;
|
||||
|
||||
/**
|
||||
* 获取供应商运行时状态
|
||||
*
|
||||
* @param array $config SMS 配置
|
||||
* @return array{configured: bool, available: bool, verified: bool, transport: string, missing_fields: array}
|
||||
*/
|
||||
public function get_status(array $config): array;
|
||||
}
|
||||
+405
@@ -0,0 +1,405 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Sms;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SMS {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_action('wp_ajax_aksrt_send_sms_code', [$this, 'ajax_send_code']);
|
||||
add_action('wp_ajax_nopriv_aksrt_send_sms_code', [$this, 'ajax_send_code']);
|
||||
add_action('wp_ajax_aksrt_sms_test', [$this, 'ajax_test_provider']);
|
||||
add_action('wp_ajax_aksrt_sms_status', [$this, 'ajax_provider_status']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取短信供应商配置
|
||||
*/
|
||||
public function get_provider_config(): array {
|
||||
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||
return \AKSRT\Settings\Settings::get_sms_settings();
|
||||
}
|
||||
return ['sms_enabled' => 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))];
|
||||
}
|
||||
|
||||
// 频率限制:间隔限制(对齐 zibll 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')];
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证短信验证码(对齐 zibll:同时校验验证码和目标手机号)
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* 错误频率限制(对齐 zibll 的 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
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* 手机号格式校验(对齐 zibll:支持中国号和国际号)
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Sms;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SMSProviderAliyun implements ProviderInterface {
|
||||
|
||||
public function get_key(): string {
|
||||
return 'aliyun';
|
||||
}
|
||||
|
||||
public function get_label(): string {
|
||||
return __('阿里云短信', 'aksrt');
|
||||
}
|
||||
|
||||
public function validate_config(array $config): array {
|
||||
$missing = [];
|
||||
if (empty($config['sms_access_key'])) $missing[] = 'sms_access_key';
|
||||
if (empty($config['sms_access_secret'])) $missing[] = 'sms_access_secret';
|
||||
if (empty($config['sms_sign_name'])) $missing[] = 'sms_sign_name';
|
||||
if (empty($config['sms_template_code'])) $missing[] = 'sms_template_code';
|
||||
return $missing;
|
||||
}
|
||||
|
||||
public function send_code(string $phone, string $code, array $config): array {
|
||||
$access_key = (string) ($config['sms_access_key'] ?? '');
|
||||
$access_secret = (string) ($config['sms_access_secret'] ?? '');
|
||||
$sign_name = (string) ($config['sms_sign_name'] ?? '');
|
||||
$template_code = (string) ($config['sms_template_code'] ?? '');
|
||||
$region_id = (string) ($config['sms_region_id'] ?? 'cn-hangzhou');
|
||||
|
||||
$params = [
|
||||
'PhoneNumbers' => $phone,
|
||||
'SignName' => $sign_name,
|
||||
'TemplateCode' => $template_code,
|
||||
'TemplateParam' => wp_json_encode(['code' => $code]),
|
||||
];
|
||||
|
||||
$result = $this->request($access_key, $access_secret, $region_id, $params);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
$this->log('error', $phone, $result->get_error_message());
|
||||
return ['success' => false, 'message' => $result->get_error_message(), 'transport' => 'signed_post'];
|
||||
}
|
||||
|
||||
// 对齐 zibll 成功判断:检查 BizId 或 Code === 'OK'
|
||||
if (!empty($result['BizId']) || (isset($result['Code']) && $result['Code'] === 'OK')) {
|
||||
$this->log('sent', $phone, 'OK');
|
||||
return ['success' => true, 'message' => __('验证码已发送', 'aksrt'), 'transport' => 'signed_post'];
|
||||
}
|
||||
|
||||
$err_code = $result['Code'] ?? 'Unknown';
|
||||
$err_msg = $result['Message'] ?? __('未知错误', 'aksrt');
|
||||
$this->log('error', $phone, "{$err_code}: {$err_msg}");
|
||||
return ['success' => false, 'message' => sprintf('阿里云: %s (%s)', $err_msg, $err_code), 'transport' => 'signed_post'];
|
||||
}
|
||||
|
||||
public function get_status(array $config): array {
|
||||
$missing = $this->validate_config($config);
|
||||
return [
|
||||
'provider' => $this->get_key(),
|
||||
'configured' => empty($missing),
|
||||
'available' => !empty($config['sms_enabled']) && empty($missing),
|
||||
'verified' => !empty($config['sms_aliyun_verified']),
|
||||
'transport' => 'signed_post',
|
||||
'missing_fields' => $missing,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送阿里云短信 API 请求(POST 方式)
|
||||
*
|
||||
* 对齐 zibll 的 SignatureHelper 实现:
|
||||
* - POST 到 https://dysmsapi.aliyuncs.com/
|
||||
* - body 为 Signature=xxx&Key=Value&... 格式
|
||||
* - 使用 x-sdk-client header
|
||||
*
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
private function request(string $access_key, string $access_secret, string $region_id, array $biz_params) {
|
||||
if (!class_exists('AKSRT\\Sms\\SMSSignatureHelper')) {
|
||||
return new \WP_Error('sms_helper_missing', '签名助手类未加载');
|
||||
}
|
||||
|
||||
$helper = new SMSSignatureHelper();
|
||||
|
||||
return $helper->request(
|
||||
$access_key,
|
||||
$access_secret,
|
||||
'dysmsapi.aliyuncs.com',
|
||||
array_merge($biz_params, [
|
||||
'RegionId' => $region_id,
|
||||
'Action' => 'SendSms',
|
||||
'Version' => '2017-05-25',
|
||||
]),
|
||||
true // HTTPS
|
||||
);
|
||||
}
|
||||
|
||||
private function log(string $level, string $phone, string $message): void {
|
||||
if (!defined('WP_DEBUG') || !WP_DEBUG) return;
|
||||
$masked = substr($phone, 0, 3) . '****' . substr($phone, -4);
|
||||
error_log(sprintf('[AKSRT SMS] [aliyun] [%s] Phone: %s — %s', $level, $masked, $message));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Sms;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SMSProviderCustom implements ProviderInterface {
|
||||
|
||||
public function get_key(): string {
|
||||
return 'custom';
|
||||
}
|
||||
|
||||
public function get_label(): string {
|
||||
return __('自定义接口', 'aksrt');
|
||||
}
|
||||
|
||||
public function validate_config(array $config): array {
|
||||
$missing = [];
|
||||
if (empty($config['sms_custom_url'])) $missing[] = 'sms_custom_url';
|
||||
return $missing;
|
||||
}
|
||||
|
||||
public function send_code(string $phone, string $code, array $config): array {
|
||||
$url = (string) ($config['sms_custom_url'] ?? '');
|
||||
$method = strtoupper((string) ($config['sms_custom_method'] ?? 'POST'));
|
||||
$auth_type = (string) ($config['sms_custom_auth_type'] ?? 'header');
|
||||
$auth_key = (string) ($config['sms_custom_auth_key'] ?? '');
|
||||
$auth_value = (string) ($config['sms_custom_auth_value'] ?? '');
|
||||
$headers_raw = (string) ($config['sms_custom_headers'] ?? '');
|
||||
$body_tpl = (string) ($config['sms_custom_body'] ?? '');
|
||||
$success_rule = (string) ($config['sms_custom_success_rule'] ?? '');
|
||||
|
||||
if ($url === '') {
|
||||
return ['success' => false, 'message' => __('自定义接口 URL 未配置', 'aksrt'), 'transport' => 'custom_http'];
|
||||
}
|
||||
|
||||
// 替换模板变量
|
||||
$replacements = [
|
||||
'{phone}' => $phone,
|
||||
'{code}' => $code,
|
||||
'{time}' => (string) time(),
|
||||
];
|
||||
|
||||
$body = str_replace(array_keys($replacements), array_values($replacements), $body_tpl);
|
||||
|
||||
// 解析自定义 headers
|
||||
$headers = ['Accept' => 'application/json'];
|
||||
if ($headers_raw !== '') {
|
||||
foreach (explode("\n", $headers_raw) as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '') continue;
|
||||
$parts = explode(':', $line, 2);
|
||||
if (count($parts) === 2) {
|
||||
$headers[trim($parts[0])] = trim(str_replace(array_keys($replacements), array_values($replacements), $parts[1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 鉴权
|
||||
if ($auth_key !== '' && $auth_value !== '') {
|
||||
$replaced_value = str_replace(array_keys($replacements), array_values($replacements), $auth_value);
|
||||
if ($auth_type === 'header') {
|
||||
$headers[$auth_key] = $replaced_value;
|
||||
}
|
||||
}
|
||||
|
||||
$args = [
|
||||
'timeout' => 10,
|
||||
'headers' => $headers,
|
||||
];
|
||||
|
||||
if (in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
|
||||
$args['body'] = $body;
|
||||
$result = wp_remote_request($url, array_merge($args, ['method' => $method]));
|
||||
} else {
|
||||
// GET — append body as query params
|
||||
$url_with_params = add_query_arg(json_decode($body, true) ?: [], $url);
|
||||
$result = wp_remote_get($url_with_params, $args);
|
||||
}
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
$this->log('error', $phone, $result->get_error_message());
|
||||
return ['success' => false, 'message' => $result->get_error_message(), 'transport' => 'custom_http'];
|
||||
}
|
||||
|
||||
$status_code = wp_remote_retrieve_response_code($result);
|
||||
$response_body = wp_remote_retrieve_body($result);
|
||||
|
||||
// 判定成功规则
|
||||
if ($success_rule !== '') {
|
||||
$matched = $this->check_success_rule($success_rule, $status_code, $response_body);
|
||||
if ($matched) {
|
||||
$this->log('sent', $phone, "status={$status_code}");
|
||||
return ['success' => true, 'message' => __('验证码已发送', 'aksrt'), 'transport' => 'custom_http'];
|
||||
}
|
||||
$this->log('error', $phone, "status={$status_code}, rule not matched");
|
||||
return ['success' => false, 'message' => sprintf(__('自定义接口返回异常 (HTTP %d)', 'aksrt'), $status_code), 'transport' => 'custom_http'];
|
||||
}
|
||||
|
||||
// 默认:HTTP 2xx 视为成功
|
||||
if ($status_code >= 200 && $status_code < 300) {
|
||||
$this->log('sent', $phone, "status={$status_code}");
|
||||
return ['success' => true, 'message' => __('验证码已发送', 'aksrt'), 'transport' => 'custom_http'];
|
||||
}
|
||||
|
||||
$this->log('error', $phone, "status={$status_code}");
|
||||
return ['success' => false, 'message' => sprintf(__('自定义接口返回 HTTP %d', 'aksrt'), $status_code), 'transport' => 'custom_http'];
|
||||
}
|
||||
|
||||
public function get_status(array $config): array {
|
||||
$missing = $this->validate_config($config);
|
||||
return [
|
||||
'provider' => $this->get_key(),
|
||||
'configured' => empty($missing),
|
||||
'available' => !empty($config['sms_enabled']) && empty($missing),
|
||||
'verified' => !empty($config['sms_custom_verified']),
|
||||
'transport' => 'custom_http',
|
||||
'missing_fields' => $missing,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验成功规则
|
||||
*
|
||||
* 支持格式:
|
||||
* - "status:200" — HTTP 状态码匹配
|
||||
* - "json:code=0" — JSON 响应中字段匹配
|
||||
* - "contains:success" — 响应体包含字符串
|
||||
*/
|
||||
private function check_success_rule(string $rule, int $status_code, string $body): bool {
|
||||
$parts = explode(':', $rule, 2);
|
||||
if (count($parts) < 2) return false;
|
||||
|
||||
$type = trim($parts[0]);
|
||||
$value = trim($parts[1]);
|
||||
|
||||
switch ($type) {
|
||||
case 'status':
|
||||
return (string) $status_code === $value;
|
||||
|
||||
case 'json':
|
||||
$json = json_decode($body, true);
|
||||
if (!is_array($json)) return false;
|
||||
$kv = explode('=', $value, 2);
|
||||
if (count($kv) !== 2) return false;
|
||||
$key = trim($kv[0]);
|
||||
$expected = trim($kv[1]);
|
||||
return (string) ($json[$key] ?? '') === $expected;
|
||||
|
||||
case 'contains':
|
||||
return strpos($body, $value) !== false;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function log(string $level, string $phone, string $message): void {
|
||||
if (!defined('WP_DEBUG') || !WP_DEBUG) return;
|
||||
$masked = substr($phone, 0, 3) . '****' . substr($phone, -4);
|
||||
error_log(sprintf('[AKSRT SMS] [custom] [%s] Phone: %s — %s', $level, $masked, $message));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Sms;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SMSProviderFactory {
|
||||
|
||||
private static $providers = [];
|
||||
|
||||
/**
|
||||
* 获取供应商 adapter 实例
|
||||
*/
|
||||
public static function get(string $key): ?ProviderInterface {
|
||||
$key = sanitize_key($key);
|
||||
if ($key === '') return null;
|
||||
|
||||
if (isset(self::$providers[$key])) {
|
||||
return self::$providers[$key];
|
||||
}
|
||||
|
||||
$instance = self::create($key);
|
||||
if ($instance !== null) {
|
||||
self::$providers[$key] = $instance;
|
||||
}
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已注册供应商
|
||||
*
|
||||
* @return ProviderInterface[]
|
||||
*/
|
||||
public static function all(): array {
|
||||
$keys = ['aliyun', 'tencent', 'huawei', 'custom'];
|
||||
$result = [];
|
||||
foreach ($keys as $key) {
|
||||
$provider = self::get($key);
|
||||
if ($provider !== null) {
|
||||
$result[$key] = $provider;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 adapter 实例
|
||||
*/
|
||||
private static function create(string $key): ?ProviderInterface {
|
||||
switch ($key) {
|
||||
case 'aliyun':
|
||||
return class_exists('AKSRT\\Sms\\SMSProviderAliyun') ? new SMSProviderAliyun() : null;
|
||||
case 'tencent':
|
||||
return class_exists('AKSRT\\Sms\\SMSProviderTencent') ? new SMSProviderTencent() : null;
|
||||
case 'huawei':
|
||||
return class_exists('AKSRT\\Sms\\SMSProviderHuawei') ? new SMSProviderHuawei() : null;
|
||||
case 'custom':
|
||||
return class_exists('AKSRT\\Sms\\SMSProviderCustom') ? new SMSProviderCustom() : null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Sms;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SMSProviderHuawei implements ProviderInterface {
|
||||
|
||||
public function get_key(): string {
|
||||
return 'huawei';
|
||||
}
|
||||
|
||||
public function get_label(): string {
|
||||
return __('华为云短信', 'aksrt');
|
||||
}
|
||||
|
||||
public function validate_config(array $config): array {
|
||||
$missing = [];
|
||||
if (empty($config['sms_access_key'])) $missing[] = 'sms_access_key';
|
||||
if (empty($config['sms_access_secret'])) $missing[] = 'sms_access_secret';
|
||||
if (empty($config['sms_huawei_sender'])) $missing[] = 'sms_huawei_sender';
|
||||
if (empty($config['sms_template_code'])) $missing[] = 'sms_template_code';
|
||||
if (empty($config['sms_huawei_signature'])) $missing[] = 'sms_huawei_signature';
|
||||
return $missing;
|
||||
}
|
||||
|
||||
public function send_code(string $phone, string $code, array $config): array {
|
||||
$app_key = (string) ($config['sms_access_key'] ?? '');
|
||||
$app_secret = (string) ($config['sms_access_secret'] ?? '');
|
||||
$sender = (string) ($config['sms_huawei_sender'] ?? '');
|
||||
$template_id = (string) ($config['sms_template_code'] ?? '');
|
||||
$signature = (string) ($config['sms_huawei_signature'] ?? '');
|
||||
$endpoint = (string) ($config['sms_huawei_endpoint'] ?? 'https://smsapi.ap-southeast-1.myhuaweicloud.com');
|
||||
|
||||
$result = $this->request($app_key, $app_secret, $sender, $template_id, $signature, $endpoint, $phone, $code);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
$this->log('error', $phone, $result->get_error_message());
|
||||
return ['success' => false, 'message' => $result->get_error_message(), 'transport' => 'hw_http'];
|
||||
}
|
||||
|
||||
$body = json_decode(wp_remote_retrieve_body($result), true);
|
||||
$code_field = $body['code'] ?? '';
|
||||
|
||||
if ($code_field === '000000') {
|
||||
$this->log('sent', $phone, '000000');
|
||||
return ['success' => true, 'message' => __('验证码已发送', 'aksrt'), 'transport' => 'hw_http'];
|
||||
}
|
||||
|
||||
$err_msg = $body['description'] ?? $body['code'] ?? __('未知错误', 'aksrt');
|
||||
$this->log('error', $phone, "{$code_field}: {$err_msg}");
|
||||
return ['success' => false, 'message' => sprintf('华为云: %s', $err_msg), 'transport' => 'hw_http'];
|
||||
}
|
||||
|
||||
public function get_status(array $config): array {
|
||||
$missing = $this->validate_config($config);
|
||||
return [
|
||||
'provider' => $this->get_key(),
|
||||
'configured' => empty($missing),
|
||||
'available' => !empty($config['sms_enabled']) && empty($missing),
|
||||
'verified' => !empty($config['sms_huawei_verified']),
|
||||
'transport' => 'hw_http',
|
||||
'missing_fields' => $missing,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 华为云 AK/SK 签名请求
|
||||
*/
|
||||
private function request(string $app_key, string $app_secret, string $sender, string $template_id, string $signature, string $endpoint, string $phone, string $code) {
|
||||
$endpoint = rtrim($endpoint, '/');
|
||||
$url = "{$endpoint}/sms/batchSendSms";
|
||||
|
||||
$body = wp_json_encode([
|
||||
'from' => $sender,
|
||||
'to' => [$phone],
|
||||
'templateId' => $template_id,
|
||||
'templateParas' => [$code],
|
||||
'signature' => $signature,
|
||||
]);
|
||||
|
||||
$timestamp = gmdate('Y-m-d\TH:i:s\Z');
|
||||
$nonce = wp_generate_uuid4();
|
||||
|
||||
// 简化签名:使用 X-Sms-AppKey 鉴权方式
|
||||
return wp_remote_post($url, [
|
||||
'timeout' => 10,
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'X-Sms-AppKey' => $app_key,
|
||||
'X-Sms-AppSecret' => $app_secret,
|
||||
],
|
||||
'body' => $body,
|
||||
]);
|
||||
}
|
||||
|
||||
private function log(string $level, string $phone, string $message): void {
|
||||
if (!defined('WP_DEBUG') || !WP_DEBUG) return;
|
||||
$masked = substr($phone, 0, 3) . '****' . substr($phone, -4);
|
||||
error_log(sprintf('[AKSRT SMS] [huawei] [%s] Phone: %s — %s', $level, $masked, $message));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Sms;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SMSProviderTencent implements ProviderInterface {
|
||||
|
||||
public function get_key(): string {
|
||||
return 'tencent';
|
||||
}
|
||||
|
||||
public function get_label(): string {
|
||||
return __('腾讯云短信', 'aksrt');
|
||||
}
|
||||
|
||||
public function validate_config(array $config): array {
|
||||
$missing = [];
|
||||
if (empty($config['sms_access_key'])) $missing[] = 'sms_access_key';
|
||||
if (empty($config['sms_access_secret'])) $missing[] = 'sms_access_secret';
|
||||
if (empty($config['sms_tencent_app_id'])) $missing[] = 'sms_tencent_app_id';
|
||||
if (empty($config['sms_sign_name'])) $missing[] = 'sms_sign_name';
|
||||
if (empty($config['sms_template_code'])) $missing[] = 'sms_template_code';
|
||||
return $missing;
|
||||
}
|
||||
|
||||
public function send_code(string $phone, string $code, array $config): array {
|
||||
$secret_id = (string) ($config['sms_access_key'] ?? '');
|
||||
$secret_key = (string) ($config['sms_access_secret'] ?? '');
|
||||
$app_id = (string) ($config['sms_tencent_app_id'] ?? '');
|
||||
$sign_name = (string) ($config['sms_sign_name'] ?? '');
|
||||
$template_id = (string) ($config['sms_template_code'] ?? '');
|
||||
$region = (string) ($config['sms_tencent_region'] ?? 'ap-guangzhou');
|
||||
|
||||
$result = $this->request($secret_id, $secret_key, $app_id, $sign_name, $template_id, $region, $phone, $code);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
$this->log('error', $phone, $result->get_error_message());
|
||||
return ['success' => false, 'message' => $result->get_error_message(), 'transport' => 'tc3_http'];
|
||||
}
|
||||
|
||||
$body = json_decode(wp_remote_retrieve_body($result), true);
|
||||
$response = $body['Response'] ?? [];
|
||||
|
||||
if (isset($response['SendStatusSet'][0]['Code']) && $response['SendStatusSet'][0]['Code'] === 'Ok') {
|
||||
$this->log('sent', $phone, 'Ok');
|
||||
return ['success' => true, 'message' => __('验证码已发送', 'aksrt'), 'transport' => 'tc3_http'];
|
||||
}
|
||||
|
||||
$err_code = $response['Error']['Code'] ?? ($response['SendStatusSet'][0]['Code'] ?? 'Unknown');
|
||||
$err_msg = $response['Error']['Message'] ?? ($response['SendStatusSet'][0]['Message'] ?? __('未知错误', 'aksrt'));
|
||||
$this->log('error', $phone, "{$err_code}: {$err_msg}");
|
||||
return ['success' => false, 'message' => sprintf('腾讯云: %s', $err_msg), 'transport' => 'tc3_http'];
|
||||
}
|
||||
|
||||
public function get_status(array $config): array {
|
||||
$missing = $this->validate_config($config);
|
||||
return [
|
||||
'provider' => $this->get_key(),
|
||||
'configured' => empty($missing),
|
||||
'available' => !empty($config['sms_enabled']) && empty($missing),
|
||||
'verified' => !empty($config['sms_tencent_verified']),
|
||||
'transport' => 'tc3_http',
|
||||
'missing_fields' => $missing,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* TC3-HMAC-SHA256 签名请求
|
||||
*/
|
||||
private function request(string $secret_id, string $secret_key, string $app_id, string $sign_name, string $template_id, string $region, string $phone, string $code) {
|
||||
$service = 'sms';
|
||||
$host = "{$service}.tencentcloudapi.com";
|
||||
$action = 'SendSms';
|
||||
$version = '2021-01-11';
|
||||
$endpoint = "https://{$host}";
|
||||
|
||||
$payload = wp_json_encode([
|
||||
'PhoneNumberSet' => [$phone],
|
||||
'SmsSdkAppId' => $app_id,
|
||||
'SignName' => $sign_name,
|
||||
'TemplateId' => $template_id,
|
||||
'TemplateParamSet' => [$code],
|
||||
]);
|
||||
|
||||
$timestamp = time();
|
||||
$date = gmdate('Y-m-d', $timestamp);
|
||||
|
||||
// Step 1: CanonicalRequest
|
||||
$http_request_method = 'POST';
|
||||
$canonical_uri = '/';
|
||||
$canonical_querystring = '';
|
||||
$content_type = 'application/json; charset=utf-8';
|
||||
$canonical_headers = "content-type:{$content_type}\nhost:{$host}\nx-tc-action:" . strtolower($action) . "\n";
|
||||
$signed_headers = 'content-type;host;x-tc-action';
|
||||
$hashed_payload = hash('sha256', $payload);
|
||||
$canonical_request = "{$http_request_method}\n{$canonical_uri}\n{$canonical_querystring}\n{$canonical_headers}\n{$signed_headers}\n{$hashed_payload}";
|
||||
|
||||
// Step 2: StringToSign
|
||||
$algorithm = 'TC3-HMAC-SHA256';
|
||||
$credential_scope = "{$date}/{$service}/tc3_request";
|
||||
$hashed_request = hash('sha256', $canonical_request);
|
||||
$string_to_sign = "{$algorithm}\n{$timestamp}\n{$credential_scope}\n{$hashed_request}";
|
||||
|
||||
// Step 3: Signature
|
||||
$secret_date = hash_hmac('sha256', $date, "TC3{$secret_key}", true);
|
||||
$secret_service = hash_hmac('sha256', $service, $secret_date, true);
|
||||
$secret_signing = hash_hmac('sha256', 'tc3_request', $secret_service, true);
|
||||
$signature = hash_hmac('sha256', $string_to_sign, $secret_signing);
|
||||
|
||||
// Step 4: Authorization header
|
||||
$authorization = "{$algorithm} Credential={$secret_id}/{$credential_scope}, SignedHeaders={$signed_headers}, Signature={$signature}";
|
||||
|
||||
return wp_remote_post($endpoint, [
|
||||
'timeout' => 10,
|
||||
'headers' => [
|
||||
'Authorization' => $authorization,
|
||||
'Content-Type' => $content_type,
|
||||
'Host' => $host,
|
||||
'X-TC-Action' => $action,
|
||||
'X-TC-Version' => $version,
|
||||
'X-TC-Timestamp' => (string) $timestamp,
|
||||
'X-TC-Region' => $region,
|
||||
],
|
||||
'body' => $payload,
|
||||
]);
|
||||
}
|
||||
|
||||
private function log(string $level, string $phone, string $message): void {
|
||||
if (!defined('WP_DEBUG') || !WP_DEBUG) return;
|
||||
$masked = substr($phone, 0, 3) . '****' . substr($phone, -4);
|
||||
error_log(sprintf('[AKSRT SMS] [tencent] [%s] Phone: %s — %s', $level, $masked, $message));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Sms;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SMSSignatureHelper {
|
||||
|
||||
/**
|
||||
* 生成签名并发起请求
|
||||
*
|
||||
* @param string $accessKeyId AccessKeyId
|
||||
* @param string $accessKeySecret AccessKeySecret
|
||||
* @param string $domain API 域名
|
||||
* @param array $params API 参数
|
||||
* @param bool $https 是否使用 HTTPS
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
public function request(string $accessKeyId, string $accessKeySecret, string $domain, array $params, bool $https = true) {
|
||||
$apiParams = array_merge([
|
||||
'SignatureMethod' => 'HMAC-SHA1',
|
||||
'SignatureNonce' => uniqid((string) mt_rand(0, 0xffff), true),
|
||||
'SignatureVersion' => '1.0',
|
||||
'AccessKeyId' => $accessKeyId,
|
||||
'Timestamp' => gmdate('Y-m-d\TH:i:s\Z'),
|
||||
'Format' => 'JSON',
|
||||
], $params);
|
||||
|
||||
ksort($apiParams);
|
||||
|
||||
$sortedQueryString = '';
|
||||
foreach ($apiParams as $key => $value) {
|
||||
$sortedQueryString .= '&' . $this->encode($key) . '=' . $this->encode((string) $value);
|
||||
}
|
||||
|
||||
$stringToSign = 'POST&%2F&' . $this->encode(substr($sortedQueryString, 1));
|
||||
$sign = base64_encode(hash_hmac('sha1', $stringToSign, $accessKeySecret . '&', true));
|
||||
$signature = $this->encode($sign);
|
||||
|
||||
$url = ($https ? 'https' : 'http') . "://{$domain}/";
|
||||
$body = "Signature={$signature}{$sortedQueryString}";
|
||||
|
||||
return $this->fetchContent($url, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* URL 编码(对齐阿里云规范)
|
||||
*/
|
||||
private function encode(string $str): string {
|
||||
$res = rawurlencode($str);
|
||||
$res = str_replace('+', '%20', $res);
|
||||
$res = str_replace('*', '%2A', $res);
|
||||
$res = str_replace('%7E', '~', $res);
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 HTTP POST 请求
|
||||
*
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
private function fetchContent(string $url, string $body) {
|
||||
$response = wp_remote_post($url, [
|
||||
'timeout' => 10,
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/x-www-form-urlencoded',
|
||||
'x-sdk-client' => 'php/2.0.0',
|
||||
],
|
||||
'body' => $body,
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$content = wp_remote_retrieve_body($response);
|
||||
$decoded = json_decode($content, true);
|
||||
|
||||
return is_array($decoded) ? $decoded : ['raw' => $content];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user