feat: batch media metadata updates and delete with usage check
lit/media-update validates the whole batch first (edit_post, lengths, optional modified lock) and restores already written items when a write fails midway. lit/media-delete requires confirm, refuses used media without force and reports refused deletes as write_failed. 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:
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Media;
|
||||
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Abilities\FollowUp;
|
||||
use LIT\MCP\Abilities\ToolResult;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* lit/media-delete: deletes one attachment after confirmation. WordPress has no media trash by
|
||||
* default (MEDIA_TRASH false): wp_delete_attachment($id, false) then deletes the attachment and
|
||||
* all its files permanently. Media that is used – certainly or possibly – needs force: true.
|
||||
* Trashed attachments are refused (MediaGuard): deleting them would always be permanent.
|
||||
*/
|
||||
final class MediaDeleteAbility implements Ability
|
||||
{
|
||||
/** @var callable(): bool Whether wp_delete_attachment($id, false) moves to the trash. */
|
||||
private $mediaTrash;
|
||||
|
||||
public function __construct(
|
||||
private MediaUsageFinder $finder,
|
||||
private ChangeLog $log,
|
||||
?callable $mediaTrash = null
|
||||
) {
|
||||
$this->mediaTrash = $mediaTrash ?? static fn (): bool => defined('MEDIA_TRASH') && MEDIA_TRASH
|
||||
&& defined('EMPTY_TRASH_DAYS') && EMPTY_TRASH_DAYS;
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'lit/media-delete';
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return 'Medium löschen';
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return 'Löscht ein Medium samt Datei und allen Bildgrößen – ohne aktivierten Medien-Papierkorb '
|
||||
. '(WordPress-Standard) endgültig, das lässt sich nicht rückgängig machen. Ohne confirm: true kommt '
|
||||
. 'eine Rückfrage (confirm_required) mit den gefundenen Verwendungen; erst nach Zustimmung des Nutzers '
|
||||
. 'mit confirm: true erneut aufrufen. Wird das Medium verwendet (auch nur "möglicherweise"), lehnt das '
|
||||
. 'Tool mit in_use ab, außer force: true. Medien im Papierkorb werden nicht gelöscht. '
|
||||
. 'Die Verwendungsprüfung findet NICHT: Term-Meta, Benutzer-Meta, Pods-Tabellen (pods_*, podsrel), '
|
||||
. 'ACF-Felder (außer als Bildfeld im Schema mit einer ID pro Meta-Eintrag), serialisierte oder '
|
||||
. 'kommagetrennte ID-Listen in Meta-Feldern und Optionen anderer Plugins – im Zweifel den Nutzer fragen.';
|
||||
}
|
||||
|
||||
public function inputSchema(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'id' => ['type' => 'integer', 'minimum' => 1],
|
||||
'confirm' => [
|
||||
'type' => 'boolean',
|
||||
'description' => 'Erst nach Rückfrage beim Nutzer true setzen.',
|
||||
],
|
||||
'force' => ['type' => 'boolean', 'description' => 'Trotz gefundener Verwendungen löschen.'],
|
||||
],
|
||||
'required' => ['id'],
|
||||
'additionalProperties' => false,
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(): ?array
|
||||
{
|
||||
return ToolResult::schema(
|
||||
['usages' => ['type' => 'array', 'items' => MediaUsageFinder::usageSchema()]],
|
||||
['usages']
|
||||
);
|
||||
}
|
||||
|
||||
public function annotations(): array
|
||||
{
|
||||
return ['readonly' => false, 'destructive' => true, 'idempotent' => false];
|
||||
}
|
||||
|
||||
public function isDirect(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
$attachment = MediaGuard::attachment($input['id'] ?? null, 'delete_post');
|
||||
|
||||
return $attachment instanceof WP_Error ? $attachment : true;
|
||||
}
|
||||
|
||||
public function execute(array $input): mixed
|
||||
{
|
||||
$attachment = MediaGuard::attachment($input['id'] ?? null, 'delete_post');
|
||||
if ($attachment instanceof WP_Error) {
|
||||
return $attachment;
|
||||
}
|
||||
|
||||
$id = (int) $attachment->ID;
|
||||
$title = (string) $attachment->post_title;
|
||||
$trash = ($this->mediaTrash)();
|
||||
// Same search as MediaUsageFinder::isUsed(): certain and possible usages both count.
|
||||
$usages = $this->finder->usages($id);
|
||||
|
||||
if (($input['confirm'] ?? false) !== true) {
|
||||
return Errors::confirmRequired($this->question($id, $title, $trash, $usages), ['usages' => $usages]);
|
||||
}
|
||||
|
||||
if ($usages !== [] && ($input['force'] ?? false) !== true) {
|
||||
return Errors::inUse(
|
||||
sprintf(
|
||||
'Medium "%s" wird noch verwendet (%d Stelle(n)); nichts wurde gelöscht.',
|
||||
$title,
|
||||
count($usages)
|
||||
),
|
||||
$usages
|
||||
);
|
||||
}
|
||||
|
||||
if (! wp_delete_attachment($id, false)) {
|
||||
return Errors::writeFailed(sprintf('WordPress hat das Löschen von Medium %d abgelehnt.', $id));
|
||||
}
|
||||
|
||||
$warnings = [];
|
||||
$summary = sprintf('Medium "%s" %s', $title, $trash ? 'in den Papierkorb gelegt' : 'gelöscht');
|
||||
$logId = FollowUp::run(
|
||||
fn (): ?int => $this->log->record($this->name(), 'attachment', (string) $id, $summary),
|
||||
$this->name() . ' change log',
|
||||
'Änderungsprotokoll konnte nicht geschrieben werden; das Medium ist trotzdem '
|
||||
. ($trash ? 'im Papierkorb.' : 'gelöscht.'),
|
||||
$warnings
|
||||
);
|
||||
|
||||
return ToolResult::ok($summary . '.')
|
||||
->changed((string) $id, 'attachment', $trash ? 'trashed' : 'deleted', $title)
|
||||
->logId(is_int($logId) ? $logId : null)
|
||||
->warnings($warnings)
|
||||
->with('usages', $usages)
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/** @param list<array<string, mixed>> $usages */
|
||||
private function question(int $id, string $title, bool $trash, array $usages): string
|
||||
{
|
||||
$message = sprintf(
|
||||
'Medium "%s" (ID %d) %s?',
|
||||
$title,
|
||||
$id,
|
||||
$trash
|
||||
? 'in den Papierkorb legen'
|
||||
: 'endgültig löschen – samt Datei und allen Bildgrößen, nicht rückgängig zu machen'
|
||||
);
|
||||
|
||||
if ($usages === []) {
|
||||
return $message . ' Keine Verwendung gefunden (die Suche deckt nicht alle Speicherorte ab).';
|
||||
}
|
||||
|
||||
$used = ' Es wird an %d Stelle(n) verwendet (siehe usages); löschen nur mit force: true.';
|
||||
|
||||
return $message . sprintf($used, count($usages));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user