\d+)', [ 'methods' => 'DELETE', 'callback' => [$this, 'rest_discard'], 'permission_callback' => static function (): bool { return is_user_logged_in(); }, 'args' => [ 'id' => ['sanitize_callback' => 'absint'], ], ]); } /** 上传成功后调用:标记为待认领。 */ public static function mark(int $attachment_id): void { if ($attachment_id > 0) { update_post_meta($attachment_id, self::META, time()); } } public function rest_discard(\WP_REST_Request $request): \WP_REST_Response { $id = absint($request->get_param('id')); if (!self::discard($id, get_current_user_id())) { return new \WP_REST_Response([ 'success' => false, 'message' => __('图片不存在或已被使用', 'jio'), ], 404); } return new \WP_REST_Response(['success' => true]); } /** 删除用户自己仍待认领的附件;已被引用的附件绝不删除。 */ public static function discard(int $attachment_id, int $user_id): bool { if (!self::is_owned_pending($attachment_id, $user_id, true)) { return false; } return (bool) wp_delete_attachment($attachment_id, true); } /** @param list $ids */ public static function claim_ids(array $ids, int $user_id): void { if ($user_id <= 0) { return; } foreach (array_unique(array_map('absint', $ids)) as $id) { if (self::is_owned_pending($id, $user_id)) { delete_post_meta($id, self::META); } } } /** * 按 URL 列表认领;已保存文章可传 null 认领全部引用。 * * @param list $urls */ public static function claim_urls(array $urls, int $user_id, ?int $limit = self::CLAIM_MAX): void { if ($user_id <= 0) { return; } $urls = array_values(array_unique($urls)); if ($limit !== null) { $urls = array_slice($urls, 0, max(0, $limit)); } $ids = []; foreach ($urls as $url) { if (is_string($url) && $url !== '') { $ids[] = (int) attachment_url_to_postid($url); } } self::claim_ids($ids, $user_id); } /** 从正文中提取本站上传目录下的 URL 并认领(评论 [img=…]、举报举证、已保存文章)。 */ public static function claim_text(string $text, int $user_id, ?int $limit = self::CLAIM_MAX): void { if ($user_id <= 0 || $text === '') { return; } $base = (string) (wp_get_upload_dir()['baseurl'] ?? ''); if ($base === '' || strpos($text, wp_parse_url($base, PHP_URL_PATH) ?: $base) === false) { return; } if (!preg_match_all('#https?://[^\s\[\]"\'<>]+#i', $text, $matches)) { return; } $urls = array_filter($matches[0], static function (string $url) use ($base): bool { return strpos(set_url_scheme($url), set_url_scheme($base)) === 0; }); self::claim_urls(array_values($urls), $user_id, $limit); } /** @param int|string $approved */ public function claim_comment($comment_id, $approved, $commentdata): void { $data = is_array($commentdata) ? $commentdata : []; self::claim_text((string) ($data['comment_content'] ?? ''), (int) ($data['user_id'] ?? 0)); } public function claim_edited_comment($comment_id, $data = []): void { $comment = get_comment((int) $comment_id); if ($comment) { self::claim_text((string) $comment->comment_content, (int) $comment->user_id); } } /** 定时清理:超过 TTL 仍未认领的附件连同文件一起删除。 */ public function cleanup(): void { global $wpdb; $ids = $wpdb->get_col($wpdb->prepare( "SELECT p.ID FROM {$wpdb->posts} p INNER JOIN {$wpdb->postmeta} m ON m.post_id = p.ID AND m.meta_key = %s WHERE p.post_type = 'attachment' AND p.post_parent = 0 AND CAST(m.meta_value AS UNSIGNED) < %d ORDER BY p.ID ASC LIMIT %d", self::META, time() - self::TTL, self::BATCH_SIZE )); foreach ((array) $ids as $id) { $id = (int) $id; $post = get_post($id); $pending_at = (int) get_post_meta($id, self::META, true); if ($post && $post->post_type === 'attachment' && (int) $post->post_parent === 0 && $pending_at > 0 && $pending_at < time() - self::TTL ) { wp_delete_attachment($id, true); } } } private static function is_owned_pending(int $attachment_id, int $user_id, bool $unattached = false): bool { if ($attachment_id <= 0 || $user_id <= 0) { return false; } $post = get_post($attachment_id); return $post && $post->post_type === 'attachment' && (int) $post->post_author === $user_id && (!$unattached || (int) $post->post_parent === 0) && get_post_meta($attachment_id, self::META, true) !== ''; } }