fix(uploads): reclaim abandoned front-submit attachments

Mark new regular and chunked uploads as pending and claim persisted article references, including partial saves and large reference sets. Protect associated media from cleanup and add upload lifecycle regression coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Fable 5 committed 2026-10-30 21:57:46 +08:00
1 parent f9b7997ec2
commit 32ac2112b0
10 files changed
+960 -26

No files matched your search

+40 -18
View File
@@ -80,34 +80,48 @@ final class PendingUploads {
/** 删除用户自己仍待认领的附件;已被引用的附件绝不删除。 */
public static function discard(int $attachment_id, int $user_id): bool {
if (!self::is_owned_pending($attachment_id, $user_id)) {
if (!self::is_owned_pending($attachment_id, $user_id, true)) {
return false;
}
return (bool) wp_delete_attachment($attachment_id, true);
}
/**
* 按 URL 列表认领(工单附件)。
*
* @param list<string> $urls
*/
public static function claim_urls(array $urls, int $user_id): void {
/** @param list<int> $ids */
public static function claim_ids(array $ids, int $user_id): void {
if ($user_id <= 0) {
return;
}
foreach (array_slice(array_unique($urls), 0, self::CLAIM_MAX) as $url) {
if (!is_string($url) || $url === '') {
continue;
}
$id = (int) attachment_url_to_postid($url);
foreach (array_unique(array_map('absint', $ids)) as $id) {
if (self::is_owned_pending($id, $user_id)) {
delete_post_meta($id, self::META);
}
}
}
/** 从正文中提取本站上传目录下的 URL 并认领(评论 [img=…]、举报举证)。 */
public static function claim_text(string $text, int $user_id): void {
/**
* 按 URL 列表认领;已保存文章可传 null 认领全部引用。
*
* @param list<string> $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;
}
@@ -121,7 +135,7 @@ final class PendingUploads {
$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);
self::claim_urls(array_values($urls), $user_id, $limit);
}
/** @param int|string $approved */
@@ -143,18 +157,25 @@ final class PendingUploads {
$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 CAST(m.meta_value AS UNSIGNED) < %d
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) {
wp_delete_attachment((int) $id, true);
$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 {
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;
}
@@ -162,6 +183,7 @@ final class PendingUploads {
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) !== '';
}
}