Files
wordpress-theme-aksrt/src/AKSRT/User/AuthApply.php
T

646 lines
26 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Frontend identity verification application.
*
* @package AKSRT
*/
namespace AKSRT\User;
defined('ABSPATH') || exit;
class AuthApply {
public const STATUS_META = 'aksrt_user_verify_status';
public const REASON_META = 'aksrt_user_verify_reason';
public const APPLY_META = 'aksrt_user_verify_apply';
public const INFO_META = 'aksrt_user_verify_info';
public const REQUESTED_AT_META = 'aksrt_user_verify_requested_at';
public const REVIEWED_AT_META = 'aksrt_user_verify_reviewed_at';
public const REVIEWER_META = 'aksrt_user_verify_reviewer';
public const HISTORY_META = 'aksrt_user_verify_history';
public const APPLICATION_COUNTS_META = 'aksrt_user_verify_application_counts';
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_user_auth_apply', [$this, 'ajax_apply']);
add_action('admin_post_aksrt_verify_evidence', [$this, 'serve_evidence']);
if (is_admin()) {
add_filter('manage_users_columns', [$this, 'add_admin_user_column']);
add_filter('manage_users_custom_column', [$this, 'render_admin_user_column'], 10, 3);
add_action('show_user_profile', [$this, 'render_admin_profile_status']);
add_action('edit_user_profile', [$this, 'render_admin_profile_status']);
}
}
/** @return array{enabled:bool,allow_apply:bool,public_display:bool,limit_level:int,desc:string,disable_desc:string,types:array<string,array{slug:string,label:string,badge_text:string,fee:float,free_applications:int,require_real_name:bool}>} */
public static function settings(): array {
$identity = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_user_identity_settings() : [];
return is_array($identity['verify'] ?? null) ? $identity['verify'] : [];
}
/** @return array<string,mixed> */
public static function type_settings(string $type): array {
$types = self::settings()['types'] ?? [];
return is_array($types[sanitize_key($type)] ?? null) ? $types[sanitize_key($type)] : [];
}
public static function application_count(int $user_id, string $type): int {
$counts = get_user_meta($user_id, self::APPLICATION_COUNTS_META, true);
$counts = is_array($counts) ? $counts : [];
return max(0, (int) ($counts[sanitize_key($type)] ?? 0));
}
private static function increment_application_count(int $user_id, string $type): void {
$counts = get_user_meta($user_id, self::APPLICATION_COUNTS_META, true);
$counts = is_array($counts) ? $counts : [];
$type = sanitize_key($type);
$counts[$type] = max(0, (int) ($counts[$type] ?? 0)) + 1;
update_user_meta($user_id, self::APPLICATION_COUNTS_META, $counts);
}
public static function get_status(int $user_id): string {
if ($user_id <= 0) {
return '';
}
return sanitize_key((string) get_user_meta($user_id, self::STATUS_META, true));
}
public static function is_verified(int $user_id): bool {
return self::get_status($user_id) === 'verified';
}
public function add_admin_user_column(array $columns): array {
$columns['aksrt_identity_status'] = __('身份认证', 'aksrt');
return $columns;
}
public function render_admin_user_column(string $output, string $column_name, int $user_id): string {
if ($column_name !== 'aksrt_identity_status') {
return $output;
}
$status = self::get_status($user_id);
$labels = self::admin_status_labels();
if ($status === '' || !isset($labels[$status])) {
return '<span style="color:#646970;">' . esc_html__('未认证', 'aksrt') . '</span>';
}
$colors = ['pending' => '#996800', 'verified' => '#008a20', 'rejected' => '#b32d2e', 'cancelled' => '#646970'];
$html = '<span style="display:inline-block;padding:2px 8px;border:1px solid currentColor;border-radius:999px;color:'
. esc_attr($colors[$status] ?? '#646970') . '">' . esc_html($labels[$status]) . '</span>';
$info = self::get_auth_info($user_id);
if ($status === 'verified' && !empty($info['name'])) {
$html .= '<div class="description">' . esc_html((string) $info['name']) . '</div>';
}
if ($status === 'pending') {
$html .= '<div><a href="' . esc_url(admin_url('users.php?page=aksrt_user_auth&status=pending&keyword=' . $user_id)) . '">' . esc_html__('查看申请', 'aksrt') . '</a></div>';
}
return $html;
}
public function render_admin_profile_status(\WP_User $user): void {
$status = self::get_status((int) $user->ID);
$labels = self::admin_status_labels();
$apply = self::get_apply_payload((int) $user->ID);
$info = self::get_auth_info((int) $user->ID);
$reason = (string) get_user_meta((int) $user->ID, self::REASON_META, true);
$reviewed_at = (string) get_user_meta((int) $user->ID, self::REVIEWED_AT_META, true);
$reviewer_id = (int) get_user_meta((int) $user->ID, self::REVIEWER_META, true);
$reviewer = $reviewer_id > 0 ? get_userdata($reviewer_id) : null;
$display = $status !== '' && isset($labels[$status]) ? $labels[$status] : __('未认证', 'aksrt');
$identity_name = (string) ($info['name'] ?? $apply['name'] ?? '');
?>
<h2><?php esc_html_e('身份认证(AKSRT)', 'aksrt'); ?></h2>
<table class="form-table" role="presentation">
<tr>
<th><?php esc_html_e('认证状态', 'aksrt'); ?></th>
<td>
<strong><?php echo esc_html($display); ?></strong>
<?php if ($status === 'pending') : ?>
<a class="button button-small" href="<?php echo esc_url(admin_url('users.php?page=aksrt_user_auth&status=pending&keyword=' . (int) $user->ID)); ?>"><?php esc_html_e('前往审核', 'aksrt'); ?></a>
<?php elseif ($status === 'verified') : ?>
<a class="button button-small" href="<?php echo esc_url(admin_url('users.php?page=aksrt_user_auth&status=verified&keyword=' . (int) $user->ID)); ?>"><?php esc_html_e('查看并管理认证', 'aksrt'); ?></a>
<?php endif; ?>
</td>
</tr>
<?php if ($identity_name !== '') : ?>
<tr><th><?php esc_html_e('认证身份', 'aksrt'); ?></th><td><?php echo esc_html($identity_name); ?></td></tr>
<?php endif; ?>
<?php if ($reviewed_at !== '') : ?>
<tr><th><?php esc_html_e('最近处理', 'aksrt'); ?></th><td><?php echo esc_html($reviewed_at); ?><?php if ($reviewer) : ?> · <?php echo esc_html($reviewer->display_name); ?><?php endif; ?></td></tr>
<?php endif; ?>
<?php if ($reason !== '') : ?>
<tr><th><?php esc_html_e('审核说明', 'aksrt'); ?></th><td><?php echo nl2br(esc_html($reason)); ?></td></tr>
<?php endif; ?>
</table>
<?php
}
/** @return array<string,string> */
private static function admin_status_labels(): array {
return [
'pending' => __('待审核', 'aksrt'),
'verified' => __('已认证', 'aksrt'),
'rejected' => __('已驳回', 'aksrt'),
'cancelled' => __('已取消', 'aksrt'),
];
}
/** @return array<string,mixed> */
public static function get_auth_info(int $user_id): array {
$info = get_user_meta($user_id, self::INFO_META, true);
return is_array($info) ? $info : [];
}
/** @return array<string,mixed> */
public static function get_apply_payload(int $user_id): array {
$apply = get_user_meta($user_id, self::APPLY_META, true);
return is_array($apply) ? $apply : [];
}
/**
* Return certification evidence only to its owner or an administrator.
*
* @param array<string,mixed> $application
* @return array<int,array{id:int,url:string,thumbnail_url:string}>
*/
public static function get_evidence_items(array $application, int $owner_id = 0, bool $admin_context = false): array {
if ($owner_id <= 0) {
return [];
}
$can_view = $admin_context
? current_user_can('manage_options')
: get_current_user_id() === $owner_id;
if (!$can_view) {
return [];
}
$items = [];
foreach ((array) ($application['evidence_attachment_ids'] ?? []) as $attachment_id) {
$attachment_id = (int) $attachment_id;
if (
$attachment_id <= 0
|| get_post_type($attachment_id) !== 'attachment'
|| (int) get_post_meta($attachment_id, '_aksrt_verify_evidence_owner', true) !== $owner_id
) {
continue;
}
$url = add_query_arg([
'action' => 'aksrt_verify_evidence',
'attachment' => $attachment_id,
'_wpnonce' => wp_create_nonce('aksrt_verify_evidence_' . $attachment_id),
], admin_url('admin-post.php'));
$items[] = [
'id' => $attachment_id,
'url' => esc_url_raw($url),
'thumbnail_url' => esc_url_raw($url),
];
}
return $items;
}
public function serve_evidence(): void {
if (!is_user_logged_in()) {
wp_die(esc_html__('请先登录', 'aksrt'), '', ['response' => 401]);
}
$attachment_id = isset($_GET['attachment']) ? (int) $_GET['attachment'] : 0;
if ($attachment_id <= 0 || !check_admin_referer('aksrt_verify_evidence_' . $attachment_id)) {
wp_die(esc_html__('安全校验失败', 'aksrt'), '', ['response' => 403]);
}
$owner_id = (int) get_post_meta($attachment_id, '_aksrt_verify_evidence_owner', true);
if ($owner_id <= 0 || (get_current_user_id() !== $owner_id && !current_user_can('manage_options'))) {
wp_die(esc_html__('您没有权限查看该证明材料', 'aksrt'), '', ['response' => 403]);
}
$file_path = get_attached_file($attachment_id);
if (!is_string($file_path) || $file_path === '' || !is_file($file_path) || !is_readable($file_path)) {
wp_die(esc_html__('证明材料不存在', 'aksrt'), '', ['response' => 404]);
}
$mime = (string) get_post_mime_type($attachment_id);
if (!in_array($mime, ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], true)) {
wp_die(esc_html__('证明材料类型无效', 'aksrt'), '', ['response' => 415]);
}
nocache_headers();
header('Content-Type: ' . $mime);
header('Content-Disposition: inline; filename="' . rawurlencode(basename($file_path)) . '"');
header('Content-Length: ' . (string) filesize($file_path));
readfile($file_path);
exit;
}
/** @param array<string,mixed> $application */
public static function application_view(array $application, int $owner_id = 0, bool $admin_context = false): array {
return [
'name' => (string) ($application['name'] ?? ''),
'real_name' => (string) ($application['real_name'] ?? ''),
'desc' => (string) ($application['desc'] ?? ''),
'msg' => (string) ($application['msg'] ?? ''),
'type' => sanitize_key((string) ($application['type'] ?? '')),
'time' => (string) ($application['time'] ?? ''),
'evidence' => self::get_evidence_items($application, $owner_id, $admin_context),
];
}
/** @return array<int,array<string,mixed>> */
public static function get_review_history(int $user_id): array {
$history = get_user_meta($user_id, self::HISTORY_META, true);
$history = is_array($history) ? array_values($history) : [];
usort($history, static fn(array $left, array $right): int => strcmp(
(string) ($right['time'] ?? ''),
(string) ($left['time'] ?? '')
));
return $history;
}
/**
* Apply one authoritative certification status transition.
*
* @return array<string,mixed>|\WP_Error
*/
public static function review(int $user_id, string $status, string $note, int $reviewer_id) {
$status = sanitize_key($status);
$note = sanitize_textarea_field($note);
if (!in_array($status, ['verified', 'rejected', 'cancelled'], true)) {
return new \WP_Error('aksrt_verify_status', __('审核状态无效', 'aksrt'));
}
if ($user_id <= 0 || !get_userdata($user_id) || $reviewer_id <= 0) {
return new \WP_Error('aksrt_verify_user', __('用户不存在', 'aksrt'));
}
if ($status === 'cancelled' && $note === '') {
return new \WP_Error('aksrt_verify_note', __('取消认证时必须填写原因', 'aksrt'));
}
if ($status === 'rejected' && $note === '') {
return new \WP_Error('aksrt_verify_note', __('驳回申请时必须填写原因', 'aksrt'));
}
$current = self::get_status($user_id);
$allowed = $status === 'cancelled'
? $current === 'verified'
: $current === 'pending';
$application = self::get_apply_payload($user_id);
if (!$allowed || $application === [] || empty($application['name']) || empty($application['type'])) {
return new \WP_Error('aksrt_verify_transition', __('该认证记录当前不可执行此操作', 'aksrt'));
}
global $wpdb;
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT umeta_id, meta_value
FROM {$wpdb->usermeta}
WHERE user_id = %d AND meta_key = %s
ORDER BY umeta_id ASC",
$user_id,
self::STATUS_META
), ARRAY_A);
if (count((array) $rows) !== 1 || sanitize_key((string) ($rows[0]['meta_value'] ?? '')) !== $current) {
return new \WP_Error('aksrt_verify_state', __('认证状态数据异常,请修复后重试', 'aksrt'));
}
$updated = $wpdb->query($wpdb->prepare(
"UPDATE {$wpdb->usermeta}
SET meta_value = %s
WHERE umeta_id = %d AND meta_value = %s",
$status,
(int) $rows[0]['umeta_id'],
$current
));
if ($updated !== 1) {
return new \WP_Error('aksrt_verify_conflict', __('认证状态已变化,请刷新后重试', 'aksrt'));
}
$reviewed_at = current_time('mysql');
update_user_meta($user_id, self::REVIEWED_AT_META, $reviewed_at);
update_user_meta($user_id, self::REVIEWER_META, $reviewer_id);
if ($note !== '') {
update_user_meta($user_id, self::REASON_META, $note);
} else {
delete_user_meta($user_id, self::REASON_META);
}
if ($status === 'verified') {
update_user_meta($user_id, self::INFO_META, [
'name' => (string) ($application['name'] ?? ''),
'desc' => (string) ($application['desc'] ?? ''),
'type' => sanitize_key((string) ($application['type'] ?? '')),
'time' => current_time('Y-m-d H:i'),
]);
} elseif ($status === 'cancelled') {
delete_user_meta($user_id, self::INFO_META);
}
$entry = [
'status' => $status,
'reason' => $note,
'reviewer' => $reviewer_id,
'time' => $reviewed_at,
'application' => self::application_view($application),
];
$history = self::get_review_history($user_id);
array_unshift($history, $entry);
update_user_meta($user_id, self::HISTORY_META, array_slice($history, 0, 50));
self::send_review_notification($user_id, $status, $note);
do_action('aksrt_user_auth_reviewed', $user_id, $status, $note, $reviewer_id);
return ['status' => $status, 'reviewed_at' => $reviewed_at];
}
private static function send_review_notification(int $user_id, string $status, string $note): void {
if (!class_exists('\\AKSRT\\Messages\\Message')) {
return;
}
if ($status === 'verified') {
$message = __('您的官方认证申请已通过审核', 'aksrt');
$title = __('官方认证审核结果', 'aksrt');
} elseif ($status === 'cancelled') {
$message = sprintf(__('您的官方认证已被取消:%s', 'aksrt'), $note);
$title = __('官方认证已取消', 'aksrt');
} else {
$message = sprintf(__('您的官方认证申请未通过:%s', 'aksrt'), $note);
$title = __('官方认证审核结果', 'aksrt');
}
\AKSRT\Messages\Message::get_instance()->send($user_id, 'auth_apply_reply', $message, [
'title' => $title,
'target_url' => aksrt_get_user_center_tab_url('auth'),
]);
}
public function ajax_apply(): void {
if (strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? '')) !== 'POST') {
wp_send_json_error(['message' => __('请求方式无效', 'aksrt')], 405);
}
if (!is_user_logged_in()) {
wp_send_json_error(['message' => __('请先登录', 'aksrt')], 401);
}
if (!check_ajax_referer('aksrt_user_auth_apply', '_wpnonce', false)) {
wp_send_json_error(['message' => __('安全校验失败', 'aksrt')], 403);
}
$user_id = get_current_user_id();
$s = self::settings();
if (!$s['enabled'] || !$s['allow_apply']) {
wp_send_json_error(['message' => __('暂不能申请身份认证', 'aksrt')], 403);
}
if (self::is_verified($user_id)) {
wp_send_json_error(['message' => __('您已是认证用户', 'aksrt')], 409);
}
if (self::get_status($user_id) === 'pending') {
wp_send_json_error(['message' => __('您的申请正在审核中', 'aksrt')], 409);
}
$name = isset($_POST['name']) ? sanitize_text_field(wp_unslash((string) $_POST['name'])) : '';
$real_name = isset($_POST['real_name']) ? sanitize_text_field(wp_unslash((string) $_POST['real_name'])) : '';
$desc = isset($_POST['desc']) ? sanitize_textarea_field(wp_unslash((string) $_POST['desc'])) : '';
$msg = isset($_POST['msg']) ? sanitize_textarea_field(wp_unslash((string) $_POST['msg'])) : '';
$type = isset($_POST['type']) ? sanitize_key((string) wp_unslash($_POST['type'])) : '';
$type_settings = self::type_settings($type);
if ($this->text_length($name) < 4 || $this->text_length($name) > 16) {
wp_send_json_error(['message' => __('认证身份长度需在 4-16 个字符之间', 'aksrt')], 422);
}
if ($this->text_length($desc) > 300 || $this->text_length($msg) > 500) {
wp_send_json_error(['message' => __('认证说明内容过长', 'aksrt')], 422);
}
if ($type_settings === []) {
wp_send_json_error(['message' => __('请选择有效的认证类型', 'aksrt')], 422);
}
if (!empty($type_settings['require_real_name']) && $real_name === '') {
wp_send_json_error(['message' => __('请填写真实姓名', 'aksrt')], 422);
}
if ($this->text_length($real_name) > 50) {
wp_send_json_error(['message' => __('真实姓名不能超过 50 个字符', 'aksrt')], 422);
}
$lock_key = 'aksrt_auth_apply_lock_' . $user_id;
$lock_token = wp_generate_uuid4();
$lock_payload = ['token' => $lock_token, 'expires' => time() + 300];
if (!add_option($lock_key, $lock_payload, '', false)) {
$current_lock = get_option($lock_key, []);
if (is_array($current_lock) && (int) ($current_lock['expires'] ?? 0) < time()) {
delete_option($lock_key);
}
if (!add_option($lock_key, $lock_payload, '', false)) {
wp_send_json_error(['message' => __('认证申请正在提交,请勿重复操作', 'aksrt')], 409);
}
}
$uploaded = [];
if (self::is_verified($user_id)) {
$this->release_submission_lock($lock_key, $lock_token);
wp_send_json_error(['message' => __('您已是认证用户', 'aksrt')], 409);
}
if (self::get_status($user_id) === 'pending') {
$this->release_submission_lock($lock_key, $lock_token);
wp_send_json_error(['message' => __('您的申请正在审核中', 'aksrt')], 409);
}
$uploaded = $this->upload_evidence($user_id);
if (is_wp_error($uploaded)) {
$this->release_submission_lock($lock_key, $lock_token);
wp_send_json_error(['message' => $uploaded->get_error_message()], 422);
}
if (!$this->owns_submission_lock($lock_key, $lock_token)) {
$this->delete_attachments($uploaded);
wp_send_json_error(['message' => __('认证申请提交超时,请重新提交', 'aksrt')], 409);
}
$this->renew_submission_lock($lock_key, $lock_token);
$free_applications = max(0, (int) ($type_settings['free_applications'] ?? 0));
$application_count = self::application_count($user_id, $type);
$fee = $application_count < $free_applications ? 0.0 : max(0, (float) ($type_settings['fee'] ?? 0));
if ($fee > 0) {
$deducted = \AKSRT\Credits\Balance::deduct(
$user_id,
$fee,
'verify_application',
['type' => 'verify', 'id' => $user_id],
sprintf(__('提交%s申请', 'aksrt'), (string) $type_settings['label'])
);
if (is_wp_error($deducted)) {
$this->delete_attachments($uploaded);
$this->release_submission_lock($lock_key, $lock_token);
wp_send_json_error(['message' => $deducted->get_error_message()], 422);
}
}
$previous_payload = self::get_apply_payload($user_id);
$requested_at = current_time('Y-m-d H:i');
$payload = [
'name' => $name,
'real_name' => $real_name,
'desc' => $desc,
'msg' => $msg,
'type' => $type,
'evidence_attachment_ids' => $uploaded,
'time' => $requested_at,
'fee' => $fee,
];
$payload_saved = update_user_meta($user_id, self::APPLY_META, $payload);
if ($payload_saved === false && get_user_meta($user_id, self::APPLY_META, true) !== $payload) {
$this->delete_attachments($uploaded);
if ($fee > 0) {
\AKSRT\Credits\Balance::add($user_id, $fee, 'verify_application_refund', ['type' => 'verify', 'id' => $user_id], __('认证申请保存失败退款', 'aksrt'));
}
$this->release_submission_lock($lock_key, $lock_token);
wp_send_json_error(['message' => __('认证申请保存失败,请重试', 'aksrt')], 500);
}
$status_saved = update_user_meta($user_id, self::STATUS_META, 'pending');
if ($status_saved === false && self::get_status($user_id) !== 'pending') {
$this->delete_attachments($uploaded);
if ($fee > 0) {
\AKSRT\Credits\Balance::add($user_id, $fee, 'verify_application_refund', ['type' => 'verify', 'id' => $user_id], __('认证申请保存失败退款', 'aksrt'));
}
$previous_payload === []
? delete_user_meta($user_id, self::APPLY_META)
: update_user_meta($user_id, self::APPLY_META, $previous_payload);
$this->release_submission_lock($lock_key, $lock_token);
wp_send_json_error(['message' => __('认证申请保存失败,请重试', 'aksrt')], 500);
}
update_user_meta($user_id, self::REQUESTED_AT_META, $requested_at);
delete_user_meta($user_id, self::REVIEWED_AT_META);
delete_user_meta($user_id, self::REVIEWER_META);
delete_user_meta($user_id, self::REASON_META);
self::increment_application_count($user_id, $type);
if (!$this->owns_submission_lock($lock_key, $lock_token)) {
$this->release_submission_lock($lock_key, $lock_token);
wp_send_json_error(['message' => __('认证申请已提交,请刷新查看最新状态', 'aksrt')], 409);
}
$this->renew_submission_lock($lock_key, $lock_token);
$this->delete_attachments(array_column(self::get_evidence_items($previous_payload, $user_id), 'id'));
/** @param int $user_id @param array $payload */
do_action('aksrt_user_auth_apply_submitted', $user_id, $payload);
$this->release_submission_lock($lock_key, $lock_token);
wp_send_json_success([
'message' => __('提交成功,请等待管理员审核', 'aksrt'),
'status' => 'pending',
], 201);
}
/** @return array<int,int>|\WP_Error */
private function upload_evidence(int $user_id) {
$files = $this->normalize_uploads($_FILES['image'] ?? null);
if (is_wp_error($files)) {
return $files;
}
if ($files === []) {
return [];
}
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
$attachment_ids = [];
foreach ($files as $file) {
$filename = sanitize_file_name((string) ($file['name'] ?? ''));
$tmp_name = (string) ($file['tmp_name'] ?? '');
$size = (int) ($file['size'] ?? 0);
$error = (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE);
if ($error !== UPLOAD_ERR_OK || $filename === '' || $size <= 0 || $size > 2 * MB_IN_BYTES || !is_uploaded_file($tmp_name)) {
$this->delete_attachments($attachment_ids);
return new \WP_Error('aksrt_auth_upload_failed', __('证明材料上传失败,单张图片不能超过 2MB', 'aksrt'));
}
$file_check = wp_check_filetype_and_ext($tmp_name, $filename);
$mime = (string) ($file_check['type'] ?? '');
if (!in_array($mime, ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], true)) {
$this->delete_attachments($attachment_ids);
return new \WP_Error('aksrt_auth_upload_type', __('证明材料仅支持 JPG、PNG、GIF 或 WebP 图片', 'aksrt'));
}
$attachment_id = media_handle_sideload([
'name' => $filename,
'tmp_name' => $tmp_name,
'type' => $mime,
'error' => UPLOAD_ERR_OK,
'size' => $size,
], 0, __('官方认证证明材料', 'aksrt'));
if (is_wp_error($attachment_id)) {
$this->delete_attachments($attachment_ids);
return new \WP_Error('aksrt_auth_upload_failed', __('证明材料写入媒体库失败', 'aksrt'));
}
$attachment_id = (int) $attachment_id;
$stored_mime = (string) get_post_mime_type($attachment_id);
if (!in_array($stored_mime, ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], true)) {
wp_delete_attachment($attachment_id, true);
$this->delete_attachments($attachment_ids);
return new \WP_Error('aksrt_auth_upload_type', __('证明材料类型校验失败', 'aksrt'));
}
wp_update_post([
'ID' => $attachment_id,
'post_author' => $user_id,
'post_status' => 'private',
]);
update_post_meta($attachment_id, '_aksrt_verify_evidence_owner', $user_id);
$attachment_ids[] = $attachment_id;
}
return $attachment_ids;
}
/** @return array<int,array<string,mixed>>|\WP_Error */
private function normalize_uploads($input) {
if (!is_array($input) || empty($input['name'])) {
return [];
}
$names = is_array($input['name']) ? $input['name'] : [$input['name']];
if (count($names) > 3) {
return new \WP_Error('aksrt_auth_upload_limit', __('证明材料最多上传 3 张', 'aksrt'));
}
$files = [];
foreach ($names as $index => $name) {
if ((string) $name === '') {
continue;
}
$files[] = [
'name' => $name,
'type' => is_array($input['type'] ?? null) ? ($input['type'][$index] ?? '') : ($input['type'] ?? ''),
'tmp_name' => is_array($input['tmp_name'] ?? null) ? ($input['tmp_name'][$index] ?? '') : ($input['tmp_name'] ?? ''),
'error' => is_array($input['error'] ?? null) ? ($input['error'][$index] ?? UPLOAD_ERR_NO_FILE) : ($input['error'] ?? UPLOAD_ERR_NO_FILE),
'size' => is_array($input['size'] ?? null) ? ($input['size'][$index] ?? 0) : ($input['size'] ?? 0),
];
}
return $files;
}
/** @param array<int,int|string> $attachment_ids */
private function delete_attachments(array $attachment_ids): void {
foreach ($attachment_ids as $attachment_id) {
$attachment_id = (int) $attachment_id;
if ($attachment_id > 0 && get_post_type($attachment_id) === 'attachment') {
wp_delete_attachment($attachment_id, true);
}
}
}
private function owns_submission_lock(string $lock_key, string $lock_token): bool {
$current_lock = get_option($lock_key, []);
return is_array($current_lock)
&& (int) ($current_lock['expires'] ?? 0) >= time()
&& hash_equals((string) ($current_lock['token'] ?? ''), $lock_token);
}
private function renew_submission_lock(string $lock_key, string $lock_token): void {
if ($this->owns_submission_lock($lock_key, $lock_token)) {
update_option($lock_key, ['token' => $lock_token, 'expires' => time() + 300], false);
}
}
private function release_submission_lock(string $lock_key, string $lock_token): void {
$current_lock = get_option($lock_key, []);
if (is_array($current_lock) && hash_equals((string) ($current_lock['token'] ?? ''), $lock_token)) {
delete_option($lock_key);
}
}
private function text_length(string $value): int {
return function_exists('mb_strlen') ? mb_strlen($value) : strlen($value);
}
}