image|video|file)', [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [$this, 'rest_list_attachments'], 'permission_callback' => [$this, 'can_list_attachments'], 'args' => [ 'type' => [ 'validate_callback' => static function ($value): bool { return in_array((string) $value, ['image', 'video', 'file'], true); }, ], 'post_id' => [ 'default' => 0, 'sanitize_callback' => 'absint', ], 'paged' => [ 'default' => 1, 'sanitize_callback' => 'absint', 'validate_callback' => static function ($value): bool { return absint($value) > 0; }, ], 'search' => [ 'default' => '', 'sanitize_callback' => 'sanitize_text_field', ], 'exclude' => [ 'default' => [], 'sanitize_callback' => [$this, 'sanitize_attachment_ids'], ], ], ]); register_rest_route('aksrt/v1', '/front-submit/upload/(?Pimage|video|resource)', [ 'methods' => 'POST', 'callback' => [$this, 'rest_upload_attachment'], 'permission_callback' => [$this, 'can_upload_attachment'], 'args' => [ 'kind' => [ 'validate_callback' => static function ($value): bool { return in_array((string) $value, ['image', 'video', 'resource'], true); }, ], 'post_id' => [ 'default' => 0, 'sanitize_callback' => 'absint', ], ], ]); } public function can_upload_attachment(\WP_REST_Request $request): bool { $user_id = get_current_user_id(); if ($user_id <= 0) { return false; } if (!$this->can_use_media($request, $user_id)) { return false; } $kind = (string) $request->get_param('kind'); $caps = [ 'image' => 'upload_submit_image', 'video' => 'new_post_upload_video', 'resource' => 'upload_resource_attachment', ]; $cap = $caps[$kind] ?? ''; if ($cap === '') { return false; } return $this->user_can_cap($cap, $user_id); } public static function upload_limit_bytes(string $kind): int { $settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : []; $configured_mb = match ($kind) { 'image' => max(1, min(20, (int) ($settings['submit_image_max_size'] ?? 5))), 'video' => max(1, min(200, (int) ($settings['submit_video_max_size'] ?? 30))), 'resource' => max(1, min(200, (int) ($settings['resource_max_size'] ?? 50))), default => 0, }; if ($configured_mb === 0) { return 0; } return min($configured_mb * MB_IN_BYTES, (int) wp_max_upload_size()); } public function rest_upload_attachment(\WP_REST_Request $request) { $kind = (string) $request->get_param('kind'); $user_id = get_current_user_id(); $files = $request->get_file_params(); $file = $files['file'] ?? null; if (!is_array($file)) { return new \WP_Error('aksrt_upload_missing_file', __('请选择要上传的文件。', 'aksrt'), ['status' => 400]); } $max_size = self::upload_limit_bytes($kind); $upload_error = (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE); if ($upload_error === UPLOAD_ERR_INI_SIZE || $upload_error === UPLOAD_ERR_FORM_SIZE) { return new \WP_Error( 'aksrt_upload_too_large', sprintf(__('文件大小不能超过 %s。', 'aksrt'), size_format($max_size)), ['status' => 413] ); } if ($upload_error === UPLOAD_ERR_PARTIAL) { return new \WP_Error('aksrt_upload_partial', __('文件未完整上传,请重新选择。', 'aksrt'), ['status' => 400]); } if ($upload_error === UPLOAD_ERR_NO_FILE || empty($file['tmp_name'])) { return new \WP_Error('aksrt_upload_missing_file', __('请选择要上传的文件。', 'aksrt'), ['status' => 400]); } if ($upload_error !== UPLOAD_ERR_OK) { return new \WP_Error('aksrt_upload_failed', __('服务器无法保存上传文件。', 'aksrt'), ['status' => 500]); } $size = (int) ($file['size'] ?? 0); if ($size <= 0 || $size > $max_size) { return new \WP_Error( 'aksrt_upload_too_large', sprintf(__('文件大小不能超过 %s。', 'aksrt'), size_format($max_size)), ['status' => 413] ); } $filename = sanitize_file_name((string) ($file['name'] ?? '')); $checked = wp_check_filetype_and_ext((string) $file['tmp_name'], $filename, get_allowed_mime_types()); $mime = (string) ($checked['type'] ?? ''); if ($filename === '' || $mime === '') { return new \WP_Error('aksrt_upload_invalid_type', __('不支持的文件类型。', 'aksrt'), ['status' => 400]); } if ($kind === 'image') { $image_info = @getimagesize((string) $file['tmp_name']); if ($image_info === false || strpos($mime, 'image/') !== 0) { return new \WP_Error('aksrt_upload_invalid_image', __('请上传有效的图片文件。', 'aksrt'), ['status' => 400]); } if (((int) $image_info[0] * (int) $image_info[1]) > 40000000) { return new \WP_Error('aksrt_upload_image_too_large', __('图片尺寸过大。', 'aksrt'), ['status' => 400]); } if (class_exists('\AKSRT\Extensions\ContentAudit')) { $audit = \AKSRT\Extensions\ContentAudit::get_instance()->audit_image((string) $file['tmp_name']); if (empty($audit['passed'])) { return new \WP_Error('aksrt_upload_image_rejected', __('图片未通过内容审核。', 'aksrt'), ['status' => 422]); } } } elseif ($kind === 'video' && strpos($mime, 'video/') !== 0) { return new \WP_Error('aksrt_upload_invalid_video', __('请上传有效的视频文件。', 'aksrt'), ['status' => 400]); } if (!function_exists('media_handle_upload')) { require_once ABSPATH . 'wp-admin/includes/media.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; } $_FILES['aksrt_front_submit_file'] = $file; $attachment_id = media_handle_upload('aksrt_front_submit_file', 0, [], ['test_form' => false]); unset($_FILES['aksrt_front_submit_file']); if (is_wp_error($attachment_id)) { return new \WP_Error('aksrt_upload_failed', $attachment_id->get_error_message(), ['status' => 400]); } $attachment_id = (int) $attachment_id; wp_update_post(['ID' => $attachment_id, 'post_author' => $user_id]); update_post_meta($attachment_id, '_aksrt_front_submit_media', $kind); $attachment = $this->prepare_attachment_response($attachment_id); if ($attachment === null) { wp_delete_attachment($attachment_id, true); return new \WP_Error('aksrt_upload_failed', __('上传后无法读取文件。', 'aksrt'), ['status' => 500]); } $attachment['success'] = true; return new \WP_REST_Response($attachment, 201); } /** * @return array */ public function settings(): array { if (class_exists('\AKSRT\Settings\Settings')) { return \AKSRT\Settings\Settings::get_submit_settings(); } return [ 'enabled' => true, 'title_min' => 5, 'title_max' => 30, 'allowed_cat_ids' => [], 'default_user_id' => 0, 'guest_enabled' => false, 'allow_category' => true, 'allow_tags' => true, 'allow_cover' => true, 'custom_filter_s' => true, 'review_mode' => 'pending', 'pay_free_s' => true, 'pay_price_min' => 0, 'pay_price_max' => 100, 'pay_vip_price_s' => true, 'pay_vip_1_enabled' => false, 'pay_vip_2_enabled' => false, 'pay_vip_1_discount' => 80, 'pay_vip_2_discount' => 50, ]; } /** * Whether the user (or guest) may open / create a submission. */ public function can_submit(?int $user_id = null): bool { $settings = $this->settings(); if (empty($settings['enabled'])) { return false; } if ($user_id === null) { $user_id = get_current_user_id(); } if ($user_id <= 0) { return !empty($settings['guest_enabled']) && (int) ($settings['default_user_id'] ?? 0) > 0; } return $this->user_can_cap('new_post_add', $user_id); } public function can_edit_post(int $post_id, ?int $user_id = null): bool { if ($post_id <= 0) { return false; } if ($user_id === null) { $user_id = get_current_user_id(); } if ($user_id <= 0) { return false; } if (user_can($user_id, 'manage_options') || user_can($user_id, 'edit_others_posts')) { return true; } $post = get_post($post_id); if (!$post || $post->post_type !== 'post') { return false; } if ((int) $post->post_author !== $user_id) { return false; } // Own drafts always editable when user can add posts. if ($post->post_status === 'draft' && $this->user_can_cap('new_post_add', $user_id)) { return true; } return $this->user_can_cap('new_post_edit', $user_id, $post_id); } public function can_delete_post(int $post_id, ?int $user_id = null): bool { if ($user_id === null) { $user_id = get_current_user_id(); } if ($user_id <= 0) { return false; } if (user_can($user_id, 'manage_options')) { return true; } $post = get_post($post_id); if (!$post || $post->post_type !== 'post') { return false; } if ((int) $post->post_author !== $user_id) { return false; } if ($post->post_status === 'draft' && $this->user_can_cap('new_post_add', $user_id)) { return true; } return $this->user_can_cap('new_post_delete', $user_id, $post_id); } public function user_can_cap(string $cap, int $user_id = 0, ?int $context = null): bool { if ($user_id > 0 && user_can($user_id, 'manage_options')) { return true; } if (function_exists('aksrt_user_can')) { return aksrt_user_can($cap, $user_id, $context); } if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'user_can')) { return \AKSRT\Settings\Settings::user_can($cap, $user_id, $context); } return $user_id > 0; } /** * @param array $data * @return array{success:bool,message:string,post_id?:int} */ public function submit(array $data): array { $action = sanitize_key((string) ($data['action_type'] ?? $data['submit_action'] ?? 'posts_save')); if ($action === 'aksrt_save_draft' || ($data['status'] ?? '') === 'draft') { $action = 'posts_draft'; } else { $action = 'posts_save'; } return $this->persist($data, $action); } /** * @param array $data * @return array{success:bool,message:string,post_id?:int} */ public function update(int $post_id, array $data): array { $data['posts_id'] = $post_id; $action = (($data['status'] ?? '') === 'draft') ? 'posts_draft' : 'posts_save'; return $this->persist($data, $action); } public function ajax_submit_post(): void { $this->verify_nonce(); $this->verify_captcha(); $data = wp_unslash($_POST); $result = $this->persist(is_array($data) ? $data : [], 'posts_save'); $this->send_result($result); } public function ajax_save_draft(): void { $this->verify_nonce(); if (!is_user_logged_in()) { wp_send_json_error(['message' => __('请先登录', 'aksrt')]); } $data = wp_unslash($_POST); $result = $this->persist(is_array($data) ? $data : [], 'posts_draft'); $this->send_result($result); } public function ajax_update_post(): void { $this->verify_nonce(); $data = wp_unslash($_POST); if (!is_array($data)) { $data = []; } $post_id = absint($data['posts_id'] ?? $data['post_id'] ?? $data['edit_id'] ?? 0); if ($post_id <= 0) { wp_send_json_error(['message' => __('文章 ID 无效', 'aksrt')]); } $data['posts_id'] = $post_id; $action = (($data['status'] ?? '') === 'draft') ? 'posts_draft' : 'posts_save'; if ($action !== 'posts_draft') { $this->verify_captcha(); } $result = $this->persist($data, $action); $this->send_result($result); } public function ajax_delete_draft(): void { $this->verify_nonce(); $user_id = get_current_user_id(); if ($user_id <= 0) { wp_send_json_error(['message' => __('请先登录', 'aksrt')]); } $post_id = absint($_POST['draft_id'] ?? $_POST['post_id'] ?? $_POST['id'] ?? 0); if ($post_id <= 0) { wp_send_json_error(['message' => __('草稿 ID 无效', 'aksrt')]); } $post = get_post($post_id); if (!$post || (int) $post->post_author !== $user_id) { wp_send_json_error(['message' => __('没有删除权限', 'aksrt')]); } if ($post->post_status !== 'draft' && !$this->can_delete_post($post_id, $user_id)) { wp_send_json_error(['message' => __('只能删除草稿', 'aksrt')]); } if ($post->post_status === 'draft') { $ok = wp_delete_post($post_id, true); } else { $ok = wp_trash_post($post_id); } if ($ok) { wp_send_json_success(['message' => __('已删除', 'aksrt')]); } wp_send_json_error(['message' => __('删除失败', 'aksrt')]); } public function ajax_delete_post(): void { $this->verify_nonce(); $user_id = get_current_user_id(); $post_id = absint($_POST['id'] ?? $_POST['post_id'] ?? 0); if (!$this->can_delete_post($post_id, $user_id)) { wp_send_json_error(['message' => __('您没有删除此内容的权限', 'aksrt')]); } $post = get_post($post_id); if (!$post || $post->post_status === 'trash') { wp_send_json_success(['message' => __('文章已经删除', 'aksrt'), 'reload' => true]); } wp_trash_post($post_id); wp_send_json_success([ 'message' => __('文章已经删除', 'aksrt'), 'reload' => true, 'goto' => home_url('/'), ]); } private function verify_nonce(): void { $nonce = (string) ($_REQUEST['_nonce'] ?? $_REQUEST['nonce'] ?? $_REQUEST['_wpnonce'] ?? ''); if ($nonce === '' || ( !wp_verify_nonce($nonce, 'aksrt_submit') && !wp_verify_nonce($nonce, 'aksrt_submit_nonce') )) { wp_send_json_error(['message' => __('安全验证失败,请刷新页面重试', 'aksrt')], 403); } } private function verify_captcha(): void { $verify = aksrt_require_captcha('front_submit', $_POST); if ($verify !== true) { $captcha_error = is_wp_error($verify) ? $verify : null; $message = $captcha_error ? $captcha_error->get_error_message() : __('人机验证失败,请重试', 'aksrt'); wp_send_json_error([ 'code' => $captcha_error ? ($captcha_error->get_error_code() ?: 'aksrt_captcha_failed') : 'aksrt_captcha_failed', 'message' => $message !== '' ? $message : __('人机验证失败,请重试', 'aksrt'), ], 400); } } /** * @param array{success:bool,message?:string} $result */ private function send_result(array $result): void { if (!empty($result['success'])) { wp_send_json_success($result); } wp_send_json_error($result); } /** * Downloads image URL into media library as featured image. */ private function set_thumbnail_from_url(int $post_id, string $url): void { if ($url === '' || !filter_var($url, FILTER_VALIDATE_URL)) { return; } if (!function_exists('media_sideload_image')) { require_once ABSPATH . 'wp-admin/includes/media.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; } $ext = strtolower(pathinfo((string) (parse_url($url, PHP_URL_PATH) ?? ''), PATHINFO_EXTENSION)); $allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp']; if ($ext !== '' && !in_array($ext, $allowed, true)) { return; } $attachment_id = media_sideload_image($url, $post_id, '', 'id'); if (!is_wp_error($attachment_id)) { set_post_thumbnail($post_id, (int) $attachment_id); } } /** * @param string[] $classes * @return string[] */ public function body_class(array $classes): array { if ($this->is_submit_template()) { $classes[] = 'aksrt-page-submit'; $classes[] = 'aksrt-hide-float'; $classes[] = 'aksrt-hide-tabbar'; } return $classes; } /** * @param array $robots * @return array */ public function robots_noindex(array $robots): array { if ($this->is_submit_template()) { $robots['noindex'] = true; $robots['nofollow'] = true; } return $robots; } /** * @param mixed $enabled * @return mixed */ public function disable_chrome_on_submit($enabled) { if ($this->is_submit_template()) { return false; } return $enabled; } public function is_submit_template(): bool { return is_page_template('templates/pages/page-submit.php'); } }