* } */ public static function settings(): array { $all = class_exists('\AKSRT\Settings\Settings') ? (array) \AKSRT\Settings\Settings::get_all() : []; $enabled = !isset($all['user_verify_enabled']) || !empty($all['user_verify_enabled']); $types_raw = (string) ($all['user_verify_apply_option'] ?? "real_name|实名认证\norganization|企业认证"); $types = []; foreach (preg_split('/\r\n|\r|\n/', $types_raw) as $line) { $line = trim($line); if ($line === '') { continue; } $parts = array_map('trim', explode('|', $line, 2)); $slug = sanitize_key($parts[0] ?? ''); $label = $parts[1] ?? $parts[0]; if ($slug === '') { continue; } $types[] = ['slug' => $slug, 'label' => (string) $label]; } if ($types === []) { $types = [ ['slug' => 'real_name', 'label' => __('实名认证', 'aksrt')], ['slug' => 'organization', 'label' => __('企业认证', 'aksrt')], ]; } return [ 'enabled' => $enabled, 'allow_apply' => !isset($all['user_verify_allow_apply']) || !empty($all['user_verify_allow_apply']), 'public_display' => !isset($all['user_verify_public_display']) || !empty($all['user_verify_public_display']), 'badge_text' => (string) ($all['user_verify_badge_text'] ?? __('已认证', 'aksrt')), 'limit_level' => max(0, (int) ($all['user_verify_limit_level'] ?? 0)), 'desc' => (string) ($all['user_verify_desc'] ?? ''), 'disable_desc' => (string) ($all['user_verify_disable_desc'] ?? ''), 'types' => $types, ]; } 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'; } /** @return array */ 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 */ 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 : []; } /** * Build the only public representation of certification evidence. * * @param array $application * @return array */ public static function get_evidence_items(array $application): array { $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') { continue; } $url = wp_get_attachment_url($attachment_id); if (!is_string($url) || $url === '') { continue; } $thumb = wp_get_attachment_image_url($attachment_id, 'thumbnail'); $items[] = [ 'id' => $attachment_id, 'url' => esc_url_raw($url), 'thumbnail_url' => esc_url_raw(is_string($thumb) && $thumb !== '' ? $thumb : $url), ]; } return $items; } /** @param array $application */ public static function application_view(array $application): array { return [ 'name' => (string) ($application['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), ]; } 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); } if ($s['limit_level'] > 0 && class_exists('\AKSRT\User\Level')) { $lv = (int) Level::get_level($user_id); if ($lv < $s['limit_level']) { wp_send_json_error([ 'message' => sprintf( /* translators: %d: required level */ __('等级不足,需达到 LV.%d 后申请', 'aksrt'), $s['limit_level'] ), ], 403); } } $name = isset($_POST['name']) ? sanitize_text_field(wp_unslash((string) $_POST['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'])) : ''; $allowed_types = array_column((array) $s['types'], 'slug'); 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 === '' || !in_array($type, $allowed_types, true)) { wp_send_json_error(['message' => __('请选择有效的认证类型', '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); $previous_payload = self::get_apply_payload($user_id); $payload = [ 'name' => $name, 'desc' => $desc, 'msg' => $msg, 'type' => $type, 'evidence_attachment_ids' => $uploaded, 'time' => current_time('Y-m-d H:i'), ]; $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); $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); $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); } 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); delete_user_meta($user_id, self::REASON_META); $this->delete_attachments(array_column(self::get_evidence_items($previous_payload), '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|\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]); update_post_meta($attachment_id, '_aksrt_verify_evidence_owner', $user_id); $attachment_ids[] = $attachment_id; } return $attachment_ids; } /** @return array>|\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 $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); } }