fix: media usage check fails closed on database errors; refused deletes checked for partial state

MediaUsageFinder treats a failed query (null result or last_error) as a possible
usage and an incomplete scan, so media-delete refuses without force. A refused
wp_delete_attachment() is write_failed only when the attachment is intact,
otherwise partial_write. media-update: re-read hint on write_failed, normalized
ids in failures, documented lock resolution and kses on restore.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
2026-09-26 02:54:37 +02:00
co-authored by Claude Opus 5.5
parent 931ddeb137
commit 7f136f6f4c
10 changed files with 238 additions and 20 deletions
+37 -1
View File
@@ -10,6 +10,7 @@ use LIT\MCP\Abilities\FollowUp;
use LIT\MCP\Abilities\ToolResult;
use LIT\MCP\ChangeLog\ChangeLog;
use WP_Error;
use WP_Post;
/**
* lit/media-delete: deletes one attachment after confirmation. WordPress has no media trash by
@@ -123,8 +124,9 @@ final class MediaDeleteAbility implements Ability
);
}
$file = (string) get_post_meta($id, '_wp_attached_file', true);
if (! wp_delete_attachment($id, false)) {
return Errors::writeFailed(sprintf('WordPress hat das Löschen von Medium %d abgelehnt.', $id));
return $this->refused($id, $title, $file);
}
$warnings = [];
@@ -145,6 +147,40 @@ final class MediaDeleteAbility implements Ability
->toArray();
}
/**
* wp_delete_attachment() returned false/null. It deletes meta, terms and the row before the
* files, so a failure can leave half a deletion: only an attachment that is still there with
* its file reference intact counts as "nothing deleted".
*/
private function refused(int $id, string $title, string $file): WP_Error
{
$reason = sprintf('WordPress hat das Löschen von Medium %d abgelehnt.', $id);
$current = get_post($id);
$intact = $current instanceof WP_Post
&& $current->post_type === 'attachment'
&& (string) get_post_meta($id, '_wp_attached_file', true) === $file;
if ($intact) {
return Errors::writeFailed($reason);
}
$this->log->recordPartial(
$this->name(),
'attachment',
(string) $id,
sprintf('Medium "%s": Löschen nur teilweise ausgeführt', $title)
);
return Errors::partialWrite(
$reason,
null,
null,
sprintf('Medium %d ist möglicherweise teilweise gelöscht (Daten oder Dateien fehlen).', $id),
'Den Aufruf nicht einfach wiederholen. Das Medium mit lit/media-get bzw. in der WordPress-Mediathek '
. 'prüfen und dem Nutzer Bescheid geben.',
['attachment_id' => $id]
);
}
/** @param list<array<string, mixed>> $usages */
private function question(int $id, string $title, bool $trash, array $usages): string
{