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) !== '';
}
}
+1 -2
View File
@@ -38,8 +38,6 @@ class FrontSubmit {
add_action('wp_ajax_nopriv_aksrt_submit_post', [$this, 'ajax_submit_post']);
add_action('rest_api_init', [$this, 'register_rest_routes']);
add_action('aksrt_new_add_posts', [$this, 'sync_post_attachments']);
add_action('aksrt_new_edit_posts', [$this, 'sync_post_attachments']);
add_filter('body_class', [$this, 'body_class']);
add_filter('wp_robots', [$this, 'robots_noindex']);
@@ -231,6 +229,7 @@ class FrontSubmit {
return new \WP_Error('aksrt_upload_failed', __('上传后无法读取文件。', 'aksrt'), ['status' => 500]);
}
\AKSRT\Attachment\PendingUploads::mark($attachment_id);
$attachment['success'] = true;
return new \WP_REST_Response($attachment, 201);
}
+8
View File
@@ -5,6 +5,8 @@
namespace AKSRT\Core;
use AKSRT\Attachment\PendingUploads;
defined('ABSPATH') || exit;
/**
@@ -197,7 +199,10 @@ trait FrontSubmitMedia {
? aksrt_get_pay_meta($post_id)
: get_post_meta($post_id, DomainModel::PAY_META_KEY, true);
$pay_meta = is_array($pay_meta) ? $pay_meta : [];
$download_urls = [];
foreach (is_array($pay_meta['downloads'] ?? null) ? $pay_meta['downloads'] : [] as $download) {
$url = (string) ($download['url'] ?? '');
if ($url !== '') $download_urls[] = $url;
$id = absint($download['attachment_id'] ?? 0);
if ($id > 0 && $this->attachment_can_attach($id, $user_id, $post_id, 'file')) {
$desired[$id] = 'file';
@@ -249,6 +254,9 @@ trait FrontSubmitMedia {
$wpdb->update($wpdb->posts, ['post_parent' => $post_id], ['ID' => $attachment_id], ['%d'], ['%d']);
clean_post_cache($attachment_id);
}
PendingUploads::claim_ids(array_keys($desired), $user_id);
PendingUploads::claim_urls($download_urls, $user_id, null);
PendingUploads::claim_text((string) $post->post_content, $user_id, null);
}
/**
+7 -3
View File
@@ -192,15 +192,19 @@ trait FrontSubmitSave {
$this->apply_custom_filters((int) $in_id, $data, $settings);
$pay_result = $this->save_pay_meta((int) $in_id, $data, $user_id, $settings);
if ($pay_result['success']) {
$this->save_featured_meta((int) $in_id, $data, $user_id);
}
// The post is already persisted even when pay validation fails.
$post = get_post((int) $in_id);
$this->sync_post_attachments($post);
if (!$pay_result['success']) {
$pay_result['post_id'] = (int) $in_id;
$pay_result['posts_id'] = (int) $in_id;
return $pay_result;
}
$this->save_featured_meta((int) $in_id, $data, $user_id);
$post = get_post((int) $in_id);
if ($post instanceof \WP_Post) {
do_action($is_edit ? 'aksrt_new_edit_posts' : 'aksrt_new_add_posts', $post);
if ($status === 'pending') {