Files
WP-LIT-MCP/src/Integrations/Media/MediaMetaWriter.php
T
janniclas.loosenandClaude Opus 5.5 7f136f6f4c 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
2026-09-26 02:54:37 +02:00

135 lines
5.0 KiB
PHP

<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Media;
use RuntimeException;
use WP_Error;
use WP_Post;
/**
* Writes the editable metadata of an attachment (alt text, title, caption, description) and
* restores it from a snapshot taken before (lit/media-update rolls a batch back with it).
* Every failure is a RuntimeException with an internal message for the PHP error log.
*
* Both apply() and restore() save through wp_update_post(), which runs the kses filters again
* for users without unfiltered_html: a caption or description with markup the current user may
* not save comes back filtered on restore (like any save of that user in the media library).
*/
final class MediaMetaWriter
{
public const ALT_KEY = '_wp_attachment_image_alt';
/** Input key => posts column. */
public const COLUMNS = ['title' => 'post_title', 'caption' => 'post_excerpt', 'description' => 'post_content'];
/**
* The current values of everything $changes will write.
*
* @param array<string, string> $changes Sanitized values by input key (alt, title, caption, description).
* @return array{id: int, columns: array<string, string>, alt_changed: bool, alt: ?string}
* alt null = there was no alt text (it is removed again on restore).
*/
public function snapshot(WP_Post $attachment, array $changes): array
{
$id = (int) $attachment->ID;
$columns = [];
foreach (self::columns($changes) as $column => $value) {
$columns[$column] = (string) $attachment->{$column};
}
$altChanged = array_key_exists('alt', $changes);
$alt = $altChanged && metadata_exists('post', $id, self::ALT_KEY)
? (string) get_post_meta($id, self::ALT_KEY, true)
: null;
return ['id' => $id, 'columns' => $columns, 'alt_changed' => $altChanged, 'alt' => $alt];
}
/**
* @param array<string, string> $changes Sanitized values by input key.
* @param bool $touched Set to true as soon as something may have been written.
* @throws RuntimeException
*/
public function apply(int $id, array $changes, bool &$touched): void
{
$columns = self::columns($changes);
if ($columns !== []) {
// Set before the call: a throwing save_post hook runs after the row was written.
$touched = true;
$result = wp_update_post(wp_slash(array_merge($columns, ['ID' => $id])), true);
if ($result instanceof WP_Error || ! $result) {
// wp_update_post() reports errors before it writes anything.
$touched = false;
throw new RuntimeException(sprintf(
'Attachment %d not updated: %s',
$id,
$result instanceof WP_Error ? $result->get_error_message() : 'unknown error'
));
}
}
if (array_key_exists('alt', $changes)) {
$touched = true;
$this->saveAlt($id, $changes['alt']);
}
}
/**
* @param array{id: int, columns: array<string, string>, alt_changed: bool, alt: ?string} $snapshot
* @throws RuntimeException
*/
public function restore(array $snapshot): void
{
$id = $snapshot['id'];
if ($snapshot['columns'] !== []) {
$result = wp_update_post(wp_slash(array_merge($snapshot['columns'], ['ID' => $id])), true);
if ($result instanceof WP_Error || ! $result) {
throw new RuntimeException(sprintf(
'Attachment %d not restored: %s',
$id,
$result instanceof WP_Error ? $result->get_error_message() : 'unknown error'
));
}
}
if (! $snapshot['alt_changed']) {
return;
}
if ($snapshot['alt'] !== null) {
$this->saveAlt($id, $snapshot['alt']);
} elseif (! delete_post_meta($id, self::ALT_KEY) && metadata_exists('post', $id, self::ALT_KEY)) {
throw new RuntimeException(sprintf('Alt text of attachment %d not removed again.', $id));
}
}
/**
* @param array<string, string> $changes
* @return array<string, string> Column => value.
*/
private static function columns(array $changes): array
{
$columns = [];
foreach (self::COLUMNS as $key => $column) {
if (array_key_exists($key, $changes)) {
$columns[$column] = $changes[$key];
}
}
return $columns;
}
/** @throws RuntimeException */
private function saveAlt(int $id, string $alt): void
{
// update_post_meta() is also false when the value is already stored.
$stored = update_post_meta($id, self::ALT_KEY, wp_slash($alt)) !== false
|| (string) get_post_meta($id, self::ALT_KEY, true) === $alt;
if (! $stored) {
throw new RuntimeException(sprintf('Alt text of attachment %d not saved.', $id));
}
}
}