posts column. */ public const COLUMNS = ['title' => 'post_title', 'caption' => 'post_excerpt', 'description' => 'post_content']; /** * The current values of everything $changes will write. * * @param array $changes Sanitized values by input key (alt, title, caption, description). * @return array{id: int, columns: array, 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 $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, 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 $changes * @return array 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)); } } }