$s ) { if ( isset( $s['url'] ) ) { $sources[ $k ]['url'] = self::with_version( (string) $s['url'], $v ); } } return $sources; } /** * Image tags in post content with the file version on src. * * @param mixed $tag Tag. * @param string $context Context. * @param int $id Attachment ID. * @return mixed */ public static function bust_tag( $tag, $context = '', $id = 0 ) { $v = self::file_version( (int) $id ); if ( '' === $v || ! is_string( $tag ) ) { return $tag; } return (string) preg_replace_callback( '/\bsrc="([^"]+)"/', static function ( $m ) use ( $v ) { return 'src="' . esc_attr( self::with_version( html_entity_decode( $m[1], ENT_QUOTES | ENT_HTML5 ), $v ) ) . '"'; }, $tag, 1 ); } /** * Remove backup files when their media item is deleted. * * @param int $id Attachment ID. */ public static function delete_backup( $id ): void { $backup = get_post_meta( (int) $id, self::BACKUP, true ); $file = (string) get_attached_file( (int) $id ); if ( ! is_array( $backup ) || empty( $backup['backups'] ) || '' === $file ) { return; } $dir = trailingslashit( dirname( $file ) ); foreach ( (array) $backup['backups'] as $bak ) { $path = $dir . wp_basename( (string) $bak ); if ( false !== strpos( wp_basename( $path ), '.bak-' ) && self::in_uploads( $path ) && is_file( $path ) ) { wp_delete_file( $path ); } } } /** * Register. * * @param Registry $r Registry. */ public static function register( Registry $r ): void { $r->add( array( 'name' => 'wp_edit_image', 'group' => 'wordpress', 'title' => 'Crop, resize or convert an image', 'description' => 'Crop, resize, rotate, flip or convert a media library image. The result is saved as a new media item (the original stays untouched) and its ID and URL are returned, so use the new ID in the page. Steps run in this order: crop, rotate, flip, resize, convert. Animated GIFs lose their animation.', 'write' => true, 'cap' => 'upload_files', 'input' => array( 'required' => array( 'id' ), 'properties' => array( 'id' => Util::id( 'Attachment ID of the image' ), 'crop' => array( 'type' => 'object', 'description' => 'Area to keep, in pixels of the image at its media URL (see width and height in wp_list_media).', 'required' => array( 'x', 'y', 'width', 'height' ), 'properties' => array( 'x' => array( 'type' => 'integer', 'minimum' => 0 ), 'y' => array( 'type' => 'integer', 'minimum' => 0 ), 'width' => array( 'type' => 'integer', 'minimum' => 1 ), 'height' => array( 'type' => 'integer', 'minimum' => 1 ), ), 'additionalProperties' => false, ), 'rotate' => array( 'type' => 'integer', 'enum' => array( 90, 180, 270 ), 'description' => 'Degrees clockwise.' ), 'flip' => array( 'type' => 'string', 'enum' => array( 'horizontal', 'vertical' ) ), 'width' => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 10000, 'description' => 'Resize to this width. With height too, the image is cropped to fill exactly that size.' ), 'height' => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 10000 ), 'format' => array( 'type' => 'string', 'enum' => array( 'webp', 'jpg', 'png' ) ), 'quality' => array( 'type' => 'integer', 'minimum' => 40, 'maximum' => 95, 'description' => 'WebP and JPEG quality (default 82).' ), 'title' => array( 'type' => 'string', 'maxLength' => 200, 'description' => 'Title of the new media item.' ), 'alt_text' => array( 'type' => 'string', 'maxLength' => 300, 'description' => 'Alt text of the new item (default: copied from the original).' ), ), ), 'run' => array( __CLASS__, 'edit' ), ) ); $r->add( array( 'name' => 'wp_regenerate_thumbnails', 'group' => 'wordpress', 'title' => 'Rebuild image thumbnails', 'description' => 'Rebuild the thumbnail sizes of up to 20 images, for example after the theme changed its image sizes or thumbnails are missing or blurry. Old thumbnail files are kept.', 'write' => true, 'idempotent' => true, 'cap' => 'upload_files', 'input' => array( 'required' => array( 'ids' ), 'properties' => array( 'ids' => array( 'type' => 'array', 'minItems' => 1, 'maxItems' => 20, 'items' => array( 'type' => 'integer', 'minimum' => 1 ) ), ), ), 'run' => array( __CLASS__, 'regenerate' ), ) ); $r->add( array( 'name' => 'wp_replace_media', 'group' => 'wordpress', 'title' => 'Replace an image file and keep its URL', 'description' => 'Swap the file of a media library image for a new picture while keeping the same media ID and URL, so every page that uses it shows the new picture. Give the new image as url, file (attached in the chat) or base64 data. The new picture is converted to the original file type. The old file is kept as a backup under a random file name in the same uploads folder until the media item is deleted: call again with restore=true to put it back (the normal undo does not restore files). If the old picture must disappear completely, delete it instead. Visitors may see the old picture until caches refresh.', 'write' => true, 'confirm' => true, 'cap' => 'upload_files', 'input' => array( 'required' => array( 'id' ), 'properties' => array( 'id' => Util::id( 'Attachment ID of the image to replace' ), 'url' => array( 'type' => 'string', 'maxLength' => 2000 ), 'file' => array( 'type' => 'object', 'properties' => array( 'download_url' => array( 'type' => 'string', 'maxLength' => 4000 ), 'file_id' => array( 'type' => 'string', 'maxLength' => 200 ), 'mime_type' => array( 'type' => 'string', 'maxLength' => 100 ), 'name' => array( 'type' => 'string', 'maxLength' => 200 ), ), 'additionalProperties' => true, ), 'data' => array( 'type' => 'string', 'maxLength' => 2000000, 'description' => 'Base64 image data, up to about 1.4 MB.' ), 'restore' => array( 'type' => 'boolean', 'description' => 'Put the file from before the last replacement back.' ), ), ), 'meta' => array( 'openai/fileParams' => array( 'file' ) ), 'run' => array( __CLASS__, 'replace' ), ) ); } /** * Image attachment the caller may edit, with its file inside the uploads folder. * * @param int $id Attachment ID. * @return array{0: \WP_Post, 1: string}|\WP_Error */ private static function image( int $id ) { $p = Util::post( $id, array( 'attachment' ) ); if ( is_wp_error( $p ) ) { return $p; } if ( ! in_array( $p->post_mime_type, self::TYPES, true ) ) { return new \WP_Error( 'not_an_image', "Media item $id is not a JPEG, PNG, GIF, WebP or AVIF image.", array( 'retryable' => false ) ); } $file = (string) get_attached_file( $p->ID ); if ( '' === $file || ! self::in_uploads( $file ) || ! is_file( $file ) ) { return new \WP_Error( 'file_missing', "The file of media item $id is missing from the uploads folder.", array( 'retryable' => false ) ); } return array( $p, $file ); } /** * Is a path inside the uploads folder? * * @param string $path Path. */ private static function in_uploads( string $path ): bool { $base = realpath( (string) wp_upload_dir( null, false )['basedir'] ); $dir = realpath( dirname( $path ) ); if ( ! $base || ! $dir ) { return false; } $base = trailingslashit( wp_normalize_path( $base ) ); return 0 === strpos( trailingslashit( wp_normalize_path( $dir ) ), $base ) && false === strpos( basename( $path ), '..' ); } private static function includes(): void { require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; } /** * Crop, rotate, flip, resize or convert into a new attachment. * * @param array $a Arguments. * @return array|\WP_Error */ public static function edit( array $a ) { $img = self::image( (int) $a['id'] ); if ( is_wp_error( $img ) ) { return $img; } list( $p, $file ) = $img; if ( empty( $a['crop'] ) && empty( $a['rotate'] ) && empty( $a['flip'] ) && empty( $a['width'] ) && empty( $a['height'] ) && empty( $a['format'] ) ) { return new \WP_Error( 'invalid_arguments', 'Pass at least one of crop, rotate, flip, width, height or format.' ); } self::includes(); // The attached file (the "-scaled" copy for big uploads), the same size wp_list_media reports. $source = $file; $editor = wp_get_image_editor( $source ); if ( is_wp_error( $editor ) ) { return new \WP_Error( 'image_editor', 'This server cannot edit images (no GD or Imagick support for this file type).', array( 'retryable' => false ) ); } $size = $editor->get_size(); if ( ! empty( $a['crop'] ) ) { $c = $a['crop']; if ( (int) $c['x'] + (int) $c['width'] > (int) $size['width'] || (int) $c['y'] + (int) $c['height'] > (int) $size['height'] ) { return new \WP_Error( 'invalid_arguments', 'The crop area is outside the image, which is ' . (int) $size['width'] . ' x ' . (int) $size['height'] . ' pixels.' ); } $res = $editor->crop( (int) $c['x'], (int) $c['y'], (int) $c['width'], (int) $c['height'] ); if ( is_wp_error( $res ) ) { return $res; } } if ( ! empty( $a['rotate'] ) ) { $res = $editor->rotate( -1 * (int) $a['rotate'] ); if ( is_wp_error( $res ) ) { return $res; } } if ( ! empty( $a['flip'] ) ) { $res = $editor->flip( 'vertical' === $a['flip'], 'horizontal' === $a['flip'] ); if ( is_wp_error( $res ) ) { return $res; } } $note = ''; if ( ! empty( $a['width'] ) || ! empty( $a['height'] ) ) { $both = ! empty( $a['width'] ) && ! empty( $a['height'] ); $res = $editor->resize( ! empty( $a['width'] ) ? (int) $a['width'] : null, ! empty( $a['height'] ) ? (int) $a['height'] : null, $both ); if ( is_wp_error( $res ) ) { if ( 'error_getting_dimensions' !== $res->get_error_code() ) { return $res; } if ( empty( $a['crop'] ) && empty( $a['rotate'] ) && empty( $a['flip'] ) && empty( $a['format'] ) ) { return new \WP_Error( 'invalid_arguments', 'Nothing to do: images are never enlarged, and this one is already smaller than that size (' . (int) $size['width'] . ' x ' . (int) $size['height'] . ' pixels).', array( 'retryable' => false ) ); } $note = ' It was not resized: images are never enlarged, and this one is already smaller than that size.'; } } $editor->set_quality( max( 40, min( 95, (int) ( $a['quality'] ?? 82 ) ) ) ); $types = array( 'webp' => 'image/webp', 'jpg' => 'image/jpeg', 'png' => 'image/png' ); $target = isset( $a['format'] ) ? $types[ (string) $a['format'] ] : (string) $p->post_mime_type; if ( ! wp_image_editor_supports( array( 'mime_type' => $target ) ) ) { return new \WP_Error( 'format_unsupported', 'This server cannot save ' . $target . ' images.', array( 'retryable' => false ) ); } $exts = array( 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp', 'image/avif' => 'avif' ); $dir = dirname( $file ); $base = preg_replace( '/-(scaled|rotated)$/', '', pathinfo( $source, PATHINFO_FILENAME ) ); $name = wp_unique_filename( $dir, sanitize_file_name( $base . '-edited.' . $exts[ $target ] ) ); if ( isset( $a['format'] ) ) { add_filter( 'image_editor_output_format', '__return_empty_array', 999 ); } $saved = $editor->save( trailingslashit( $dir ) . $name, $target ); remove_filter( 'image_editor_output_format', '__return_empty_array', 999 ); if ( is_wp_error( $saved ) ) { return $saved; } $alt = isset( $a['alt_text'] ) ? (string) $a['alt_text'] : (string) get_post_meta( $p->ID, '_wp_attachment_image_alt', true ); $title = isset( $a['title'] ) && '' !== trim( (string) $a['title'] ) ? (string) $a['title'] : $p->post_title . ' (edited)'; $new = wp_insert_attachment( array( 'post_mime_type' => $saved['mime-type'], 'post_title' => sanitize_text_field( $title ), 'post_content' => '', 'post_excerpt' => $p->post_excerpt, 'post_status' => 'inherit', ), $saved['path'], (int) $p->post_parent, true ); if ( is_wp_error( $new ) ) { wp_delete_file( $saved['path'] ); return $new; } if ( isset( $a['format'] ) ) { add_filter( 'image_editor_output_format', '__return_empty_array', 999 ); } wp_update_attachment_metadata( $new, wp_generate_attachment_metadata( $new, $saved['path'] ) ); remove_filter( 'image_editor_output_format', '__return_empty_array', 999 ); if ( '' !== $alt ) { update_post_meta( $new, '_wp_attachment_image_alt', sanitize_text_field( $alt ) ); } $out = WordPress::media_item( get_post( $new ) ); $out['original'] = $p->ID; $out['message'] = 'Saved as new media item ' . $new . '. The original (' . $p->ID . ') is unchanged.' . $note; return $out; } /** * Rebuild thumbnails. * * @param array $a Arguments. * @return array */ public static function regenerate( array $a ): array { self::includes(); $done = array(); $failed = array(); foreach ( array_unique( array_map( 'intval', (array) $a['ids'] ) ) as $id ) { $img = self::image( $id ); if ( is_wp_error( $img ) ) { $failed[] = array( 'id' => $id, 'error' => $img->get_error_message() ); continue; } if ( is_array( get_post_meta( $id, self::BACKUP, true ) ) ) { // A replaced image keeps its URL: no new "-scaled" copy. self::rebuild( $id, $img[1] ); $meta = wp_get_attachment_metadata( $id ); $done[] = array( 'id' => $id, 'sizes' => count( (array) ( $meta['sizes'] ?? array() ) ) ); continue; } $source = function_exists( 'wp_get_original_image_path' ) ? (string) wp_get_original_image_path( $id ) : ''; $source = '' !== $source && self::in_uploads( $source ) && is_file( $source ) ? $source : $img[1]; $meta = wp_generate_attachment_metadata( $id, $source ); if ( empty( $meta ) || is_wp_error( $meta ) ) { $failed[] = array( 'id' => $id, 'error' => 'The thumbnails could not be created.' ); continue; } wp_update_attachment_metadata( $id, $meta ); $done[] = array( 'id' => $id, 'sizes' => count( (array) ( $meta['sizes'] ?? array() ) ) ); } return array( 'rebuilt' => $done, 'failed' => $failed ); } /** * New image into a temporary file. * * @param array $a Arguments. * @return string|\WP_Error Temporary file. */ private static function fetch( array $a ) { $url = (string) ( $a['url'] ?? '' ); $data = (string) ( $a['data'] ?? '' ); if ( '' === $url && ! empty( $a['file'] ) && is_array( $a['file'] ) ) { $url = (string) ( $a['file']['download_url'] ?? '' ); } if ( '' !== $data ) { $data = (string) preg_replace( '#^data:[^;,]+;base64,#i', '', trim( $data ) ); $bytes = base64_decode( (string) preg_replace( '/\s+/', '', $data ), true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode if ( false === $bytes || strlen( $bytes ) < 20 || strlen( $bytes ) > 1500000 ) { return new \WP_Error( 'bad_data', 'The image data is not valid base64 or is too large.' ); } global $wp_filesystem; $tmp = wp_tempnam( 'handymcp-replace' ); if ( ! $tmp || ! WP_Filesystem() || ! $wp_filesystem || ! $wp_filesystem->put_contents( $tmp, $bytes, FS_CHMOD_FILE ) ) { if ( $tmp ) { wp_delete_file( $tmp ); } return new \WP_Error( 'write_failed', 'The image could not be saved to a temporary file.' ); } return $tmp; } if ( '' === $url ) { return new \WP_Error( 'missing', 'Pass url, file or data with the new image.' ); } if ( ! WordPress::public_url( $url ) ) { return new \WP_Error( 'blocked_url', 'Only public http(s) URLs can be downloaded.' ); } return WordPress::safe_download( $url ); } /** * Files that belong to an attachment's current version (main file, original and sizes). * * @param string $file Main file. * @param array $meta Metadata. * @return string[] */ private static function version_files( string $file, array $meta ): array { $dir = trailingslashit( dirname( $file ) ); $files = array( $file ); if ( ! empty( $meta['original_image'] ) ) { $files[] = $dir . wp_basename( (string) $meta['original_image'] ); } foreach ( (array) ( $meta['sizes'] ?? array() ) as $s ) { if ( ! empty( $s['file'] ) ) { $files[] = $dir . wp_basename( (string) $s['file'] ); } } return array_values( array_unique( array_filter( $files, array( __CLASS__, 'in_uploads' ) ) ) ); } /** * Replace the file, or restore the backup. * * @param array $a Arguments. * @return array|\WP_Error */ public static function replace( array $a ) { $img = self::image( (int) $a['id'] ); if ( is_wp_error( $img ) ) { return $img; } list( $p, $file ) = $img; self::includes(); $lock = 'handymcp_replace_lock_' . $p->ID; if ( ! add_option( $lock, time(), '', false ) ) { if ( (int) get_option( $lock ) > time() - 5 * MINUTE_IN_SECONDS ) { return new \WP_Error( 'busy', 'This image is being replaced right now. Try again in a minute.' ); } update_option( $lock, time(), false ); } try { return History::without( static function () use ( $a, $p, $file ) { $out = ! empty( $a['restore'] ) ? self::restore( $p, $file ) : self::swap( $a, $p, $file ); if ( ! is_wp_error( $out ) ) { update_post_meta( $p->ID, self::VER, (string) time() ); } return $out; } ); } finally { delete_option( $lock ); } } /** * Move a file inside the uploads folder. * * @param string $from From. * @param string $to To. */ private static function move( string $from, string $to ): bool { global $wp_filesystem; if ( ! WP_Filesystem() || ! $wp_filesystem || ! self::in_uploads( $to ) ) { return false; } if ( ! $wp_filesystem->move( $from, $to, true ) ) { return false; } $wp_filesystem->chmod( $to, defined( 'FS_CHMOD_FILE' ) ? FS_CHMOD_FILE : 0644 ); return true; } /** * Rebuild sizes for the file at the attachment's current path, without making a "-scaled" copy. * * @param int $id Attachment ID. * @param string $file Main file. */ private static function rebuild( int $id, string $file ): void { add_filter( 'big_image_size_threshold', '__return_false', 999 ); add_filter( 'image_editor_output_format', '__return_empty_array', 999 ); $meta = wp_generate_attachment_metadata( $id, $file ); remove_filter( 'image_editor_output_format', '__return_empty_array', 999 ); remove_filter( 'big_image_size_threshold', '__return_false', 999 ); if ( is_array( $meta ) ) { unset( $meta['original_image'] ); wp_update_attachment_metadata( $id, $meta ); } update_attached_file( $id, $file ); } /** * Put the new picture in place. * * @param array $a Arguments. * @param \WP_Post $p Attachment. * @param string $file Main file. * @return array|\WP_Error */ private static function swap( array $a, \WP_Post $p, string $file ) { $tmp = self::fetch( $a ); if ( is_wp_error( $tmp ) ) { return $tmp; } $mime = function_exists( 'wp_get_image_mime' ) ? (string) wp_get_image_mime( $tmp ) : ''; if ( ! in_array( $mime, self::TYPES, true ) ) { wp_delete_file( $tmp ); return new \WP_Error( 'not_an_image', 'The new file is not a JPEG, PNG, GIF, WebP or AVIF image.', array( 'retryable' => false ) ); } $editor = wp_get_image_editor( $tmp ); if ( is_wp_error( $editor ) ) { wp_delete_file( $tmp ); return new \WP_Error( 'image_editor', 'This server cannot read the new image.', array( 'retryable' => false ) ); } $ready = $tmp . '.ready'; // Same file type as before, whatever the site converts new uploads to, so the URL keeps working. add_filter( 'image_editor_output_format', '__return_empty_array', 999 ); $saved = $editor->save( $ready, (string) $p->post_mime_type ); remove_filter( 'image_editor_output_format', '__return_empty_array', 999 ); wp_delete_file( $tmp ); if ( is_wp_error( $saved ) || empty( $saved['path'] ) || ! is_file( $saved['path'] ) ) { return new \WP_Error( 'convert_failed', 'The new image could not be converted to ' . $p->post_mime_type . '.' ); } $ready = (string) $saved['path']; // One backup at a time: a second replacement keeps the first original as the backup. $backup = get_post_meta( $p->ID, self::BACKUP, true ); $meta = (array) wp_get_attachment_metadata( $p->ID ); $old = self::version_files( $file, $meta ); if ( ! is_array( $backup ) || empty( $backup['backups'] ) ) { // Long random name, so the old picture cannot be guessed from the image URL. $tag = strtolower( wp_generate_password( 24, false, false ) ); $moved = array(); foreach ( $old as $f ) { if ( ! is_file( $f ) ) { continue; } $to = dirname( $f ) . '/' . pathinfo( $f, PATHINFO_FILENAME ) . '.bak-' . $tag . '.' . pathinfo( $f, PATHINFO_EXTENSION ); if ( ! self::move( $f, $to ) ) { foreach ( $moved as $back => $from ) { self::move( $back, $from ); } wp_delete_file( $ready ); return new \WP_Error( 'filesystem', 'The current file could not be backed up, so nothing was changed.' ); } $moved[ $to ] = $f; } update_post_meta( $p->ID, self::BACKUP, array( 'originals' => array_values( array_map( 'wp_basename', $moved ) ), 'backups' => array_values( array_map( 'wp_basename', array_keys( $moved ) ) ), 'meta' => $meta, 'file' => (string) get_post_meta( $p->ID, '_wp_attached_file', true ), 'time' => time(), ) ); $aside = array(); } else { // The first original stays the backup; the current file is set aside until the new one is in place. $aside = array(); foreach ( $old as $f ) { if ( is_file( $f ) && self::move( $f, $f . '.handymcp-tmp' ) ) { $aside[ $f . '.handymcp-tmp' ] = $f; } } } if ( ! self::move( $ready, $file ) ) { wp_delete_file( $ready ); if ( $aside ) { foreach ( $aside as $tmp_file => $f ) { self::move( $tmp_file, $f ); } return new \WP_Error( 'filesystem', 'The new file could not be saved, so nothing was changed.' ); } $back = self::restore( $p, $file ); return new \WP_Error( 'filesystem', is_wp_error( $back ) ? 'The new file could not be saved, and the old file could not be put back automatically. Call wp_replace_media with restore=true.' : 'The new file could not be saved, so the old one was put back.' ); } foreach ( array_keys( $aside ) as $tmp_file ) { wp_delete_file( $tmp_file ); } self::rebuild( $p->ID, $file ); Util::purge_post( $p->ID ); $out = WordPress::media_item( get_post( $p->ID ) ); $out['message'] = 'Replaced. The URL stays the same. Browsers and caches may show the old picture for a while. To undo, call wp_replace_media with restore=true.'; return $out; } /** * Put the backup back. * * @param \WP_Post $p Attachment. * @param string $file Main file. * @return array|\WP_Error */ private static function restore( \WP_Post $p, string $file ) { $backup = get_post_meta( $p->ID, self::BACKUP, true ); if ( ! is_array( $backup ) || empty( $backup['backups'] ) || empty( $backup['originals'] ) ) { return new \WP_Error( 'no_backup', 'There is no backup of an earlier file for this media item.', array( 'retryable' => false ) ); } $dir = trailingslashit( dirname( $file ) ); $originals = array_values( (array) $backup['originals'] ); $pairs = array(); foreach ( array_values( (array) $backup['backups'] ) as $i => $bak ) { $from = $dir . wp_basename( (string) $bak ); $to = $dir . wp_basename( (string) ( $originals[ $i ] ?? '' ) ); if ( false !== strpos( wp_basename( $from ), '.bak-' ) && $to !== $dir && is_file( $from ) ) { $pairs[ $from ] = $to; } } if ( ! $pairs ) { return new \WP_Error( 'no_backup', 'The backup files are missing, so nothing was changed.', array( 'retryable' => false ) ); } // Set the current files aside, so a failed restore can be rolled back. $aside = array(); foreach ( self::version_files( $file, (array) wp_get_attachment_metadata( $p->ID ) ) as $f ) { if ( is_file( $f ) && self::move( $f, $f . '.handymcp-tmp' ) ) { $aside[ $f . '.handymcp-tmp' ] = $f; } } $done = array(); foreach ( $pairs as $from => $to ) { if ( ! self::move( $from, $to ) ) { foreach ( $done as $d_from => $d_to ) { self::move( $d_to, $d_from ); } foreach ( $aside as $tmp_file => $f ) { self::move( $tmp_file, $f ); } return new \WP_Error( 'filesystem', 'The backup could not be put back, so nothing was changed.' ); } $done[ $from ] = $to; } foreach ( array_keys( $aside ) as $tmp_file ) { wp_delete_file( $tmp_file ); } if ( ! empty( $backup['file'] ) ) { update_post_meta( $p->ID, '_wp_attached_file', (string) $backup['file'] ); } wp_update_attachment_metadata( $p->ID, (array) $backup['meta'] ); delete_post_meta( $p->ID, self::BACKUP ); Util::purge_post( $p->ID ); $out = WordPress::media_item( get_post( $p->ID ) ); $out['message'] = 'The earlier file is back.'; return $out; } }