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 Annotations::destructive(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 ); } $file = (string) get_post_meta($id, '_wp_attached_file', true); if (! wp_delete_attachment($id, false)) { return $this->refused($id, $title, $file); } $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(); } /** * 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> $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)); } }