From 5c80fcf01c46fdf89d0f64a9964e8ac7160cdeb2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:04:32 +0000 Subject: [PATCH 1/6] Initial plan From 6c4e9a0494bf1db923f4728d52948fa82ea5a8bd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:16:12 +0000 Subject: [PATCH 2/6] Add --update-attachment-refs flag to wp media regenerate Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/media-regenerate.feature | 100 ++++++++++++++++++++++++++++++ src/Media_Command.php | 73 +++++++++++++++++++++- 2 files changed, 170 insertions(+), 3 deletions(-) diff --git a/features/media-regenerate.feature b/features/media-regenerate.feature index 6b48d1ad..edec0726 100644 --- a/features/media-regenerate.feature +++ b/features/media-regenerate.feature @@ -1933,3 +1933,103 @@ Feature: Regenerate WordPress attachments """ site_icon-270 """ + + Scenario: Update post content references when regenerating a specific image size + Given download: + | path | url | + | {CACHE_DIR}/canola.jpg | http://wp-cli.org/behat-data/canola.jpg | + And a wp-content/mu-plugins/media-settings.php file: + """ + =' ) ) { @@ -212,7 +217,7 @@ public function regenerate( $args, $assoc_args = array() ) { // @phpstan-ignore function.deprecated Utils\wp_clear_object_cache(); } - $this->process_regeneration( $post_id, $skip_delete, $only_missing, $delete_unknown, $image_sizes, $number . '/' . $count, $successes, $errors, $skips ); + $this->process_regeneration( $post_id, $skip_delete, $only_missing, $delete_unknown, $image_sizes, $update_attachment_refs, $number . '/' . $count, $successes, $errors, $skips ); } if ( isset( $image_size_filters ) ) { @@ -715,6 +720,7 @@ private function get_image_sizes_description( array $sizes, $noun, $default_if_e * @param bool $only_missing * @param bool $delete_unknown * @param string[] $image_sizes + * @param bool $update_attachment_refs * @param string $progress * @param int $successes * @param int $errors @@ -724,7 +730,7 @@ private function get_image_sizes_description( array $sizes, $noun, $default_if_e * @param-out int $skips * @return void */ - private function process_regeneration( $id, $skip_delete, $only_missing, $delete_unknown, $image_sizes, $progress, &$successes, &$errors, &$skips ) { + private function process_regeneration( $id, $skip_delete, $only_missing, $delete_unknown, $image_sizes, $update_attachment_refs, $progress, &$successes, &$errors, &$skips ) { $title = get_the_title( $id ); if ( '' === $title ) { @@ -752,6 +758,20 @@ private function process_regeneration( $id, $skip_delete, $only_missing, $delete $original_meta = wp_get_attachment_metadata( $id ); + $old_size_urls = array(); + if ( $update_attachment_refs && is_array( $original_meta ) && ! empty( $original_meta['sizes'] ) ) { + $attachment_url = wp_get_attachment_url( $id ); + if ( $attachment_url ) { + $dir_url = trailingslashit( dirname( $attachment_url ) ); + $sizes_to_track = $image_sizes ?: array_keys( $original_meta['sizes'] ); + foreach ( $sizes_to_track as $size ) { + if ( ! empty( $original_meta['sizes'][ $size ]['file'] ) ) { + $old_size_urls[ $size ] = $dir_url . $original_meta['sizes'][ $size ]['file']; + } + } + } + } + if ( $delete_unknown ) { $this->delete_unknown_image_sizes( $id, $fullsizepath ); @@ -846,6 +866,31 @@ private function process_regeneration( $id, $skip_delete, $only_missing, $delete WP_CLI::log( "$progress Regenerated thumbnails for $att_desc." ); } + + if ( $update_attachment_refs && ! empty( $old_size_urls ) && is_array( $metadata ) && ! empty( $metadata['sizes'] ) ) { + $attachment_url = wp_get_attachment_url( $id ); + if ( $attachment_url ) { + $dir_url = trailingslashit( dirname( $attachment_url ) ); + /** + * @var array> $new_sizes + */ + $new_sizes = is_array( $metadata['sizes'] ) ? $metadata['sizes'] : array(); + foreach ( $old_size_urls as $size => $old_url ) { + $size_data = $new_sizes[ $size ] ?? null; + if ( ! is_array( $size_data ) || empty( $size_data['file'] ) ) { + continue; + } + /** + * @var array{file: string} $size_data + */ + $new_url = $dir_url . $size_data['file']; + if ( $old_url !== $new_url ) { + $this->update_post_content_for_attachment( $old_url, $new_url ); + } + } + } + } + ++$successes; } @@ -1762,4 +1807,26 @@ private function delete_unknown_image_sizes( $id, $fullsizepath ) { // @phpstan-ignore argument.type wp_update_attachment_metadata( $id, $original_meta ); } + + /** + * Updates post content replacing an old attachment URL with a new one. + * + * @param string $old_url Old thumbnail URL to search for. + * @param string $new_url New thumbnail URL to replace with. + * @return void + */ + private function update_post_content_for_attachment( $old_url, $new_url ) { + global $wpdb; + $result = $wpdb->query( + $wpdb->prepare( + "UPDATE {$wpdb->posts} SET post_content = REPLACE(post_content, %s, %s) WHERE post_content LIKE %s", + $old_url, + $new_url, + '%' . $wpdb->esc_like( $old_url ) . '%' + ) + ); + if ( false === $result ) { + WP_CLI::warning( sprintf( 'Failed to update post content references from "%s" to "%s".', $old_url, $new_url ) ); + } + } } From 7d89a2be5698f14bfbed014536922a358c12b70b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 09:47:42 +0000 Subject: [PATCH 3/6] Batch URL replacements into single UPDATE per attachment Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Media_Command.php | 49 +++++++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/src/Media_Command.php b/src/Media_Command.php index b3803c63..967870c1 100644 --- a/src/Media_Command.php +++ b/src/Media_Command.php @@ -874,7 +874,8 @@ private function process_regeneration( $id, $skip_delete, $only_missing, $delete /** * @var array> $new_sizes */ - $new_sizes = is_array( $metadata['sizes'] ) ? $metadata['sizes'] : array(); + $new_sizes = is_array( $metadata['sizes'] ) ? $metadata['sizes'] : array(); + $url_replacements = array(); foreach ( $old_size_urls as $size => $old_url ) { $size_data = $new_sizes[ $size ] ?? null; if ( ! is_array( $size_data ) || empty( $size_data['file'] ) ) { @@ -885,9 +886,12 @@ private function process_regeneration( $id, $skip_delete, $only_missing, $delete */ $new_url = $dir_url . $size_data['file']; if ( $old_url !== $new_url ) { - $this->update_post_content_for_attachment( $old_url, $new_url ); + $url_replacements[ $old_url ] = $new_url; } } + if ( ! empty( $url_replacements ) ) { + $this->update_post_content_for_attachment( $url_replacements ); + } } } @@ -1809,24 +1813,43 @@ private function delete_unknown_image_sizes( $id, $fullsizepath ) { } /** - * Updates post content replacing an old attachment URL with a new one. + * Updates post content replacing old attachment URLs with new ones in a single query. + * + * Applies all replacements as nested REPLACE() calls so only one table scan is needed. * - * @param string $old_url Old thumbnail URL to search for. - * @param string $new_url New thumbnail URL to replace with. + * @param array $url_replacements Map of old URL => new URL. * @return void */ - private function update_post_content_for_attachment( $old_url, $new_url ) { + private function update_post_content_for_attachment( array $url_replacements ) { global $wpdb; + + if ( empty( $url_replacements ) ) { + return; + } + + $replace_expr = 'post_content'; + $replace_args = array(); + $where_clauses = array(); + $where_args = array(); + + foreach ( $url_replacements as $old_url => $new_url ) { + $replace_expr = "REPLACE($replace_expr, %s, %s)"; + $replace_args[] = $old_url; + $replace_args[] = $new_url; + $where_clauses[] = 'post_content LIKE %s'; + $where_args[] = '%' . $wpdb->esc_like( $old_url ) . '%'; + } + + $where_sql = implode( ' OR ', $where_clauses ); + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare $result = $wpdb->query( - $wpdb->prepare( - "UPDATE {$wpdb->posts} SET post_content = REPLACE(post_content, %s, %s) WHERE post_content LIKE %s", - $old_url, - $new_url, - '%' . $wpdb->esc_like( $old_url ) . '%' - ) + $wpdb->prepare( "UPDATE {$wpdb->posts} SET post_content = {$replace_expr} WHERE {$where_sql}", ...array_merge( $replace_args, $where_args ) ) ); + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare if ( false === $result ) { - WP_CLI::warning( sprintf( 'Failed to update post content references from "%s" to "%s".', $old_url, $new_url ) ); + WP_CLI::warning( 'Failed to update post content references for attachment.' ); + } else { + wp_cache_set( 'last_changed', microtime(), 'posts' ); } } } From 2bae8c798e6257545ae32f54ce15167f7933d840 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 20 Mar 2026 13:03:51 +0100 Subject: [PATCH 4/6] Update src/Media_Command.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Media_Command.php | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Media_Command.php b/src/Media_Command.php index 967870c1..0c0ffc26 100644 --- a/src/Media_Command.php +++ b/src/Media_Command.php @@ -1841,14 +1841,31 @@ private function update_post_content_for_attachment( array $url_replacements ) { } $where_sql = implode( ' OR ', $where_clauses ); + + // First, find the IDs of posts whose content will be updated so we can clear their object cache entries. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare + $post_ids = $wpdb->get_col( + $wpdb->prepare( + "SELECT ID FROM {$wpdb->posts} WHERE {$where_sql}", + ...$where_args + ) + ); + $result = $wpdb->query( - $wpdb->prepare( "UPDATE {$wpdb->posts} SET post_content = {$replace_expr} WHERE {$where_sql}", ...array_merge( $replace_args, $where_args ) ) + $wpdb->prepare( + "UPDATE {$wpdb->posts} SET post_content = {$replace_expr} WHERE {$where_sql}", + ...array_merge( $replace_args, $where_args ) + ) ); // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare if ( false === $result ) { WP_CLI::warning( 'Failed to update post content references for attachment.' ); } else { + if ( ! empty( $post_ids ) ) { + foreach ( $post_ids as $post_id ) { + clean_post_cache( (int) $post_id ); + } + } wp_cache_set( 'last_changed', microtime(), 'posts' ); } } From 285e150ebc5d6bec6c10746c38ae22d0403ef7d5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:06:03 +0000 Subject: [PATCH 5/6] Exclude revisions from post content URL replacement queries Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Media_Command.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Media_Command.php b/src/Media_Command.php index 0c0ffc26..d53da53d 100644 --- a/src/Media_Command.php +++ b/src/Media_Command.php @@ -1846,14 +1846,14 @@ private function update_post_content_for_attachment( array $url_replacements ) { // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare $post_ids = $wpdb->get_col( $wpdb->prepare( - "SELECT ID FROM {$wpdb->posts} WHERE {$where_sql}", + "SELECT ID FROM {$wpdb->posts} WHERE post_type <> 'revision' AND ({$where_sql})", ...$where_args ) ); $result = $wpdb->query( $wpdb->prepare( - "UPDATE {$wpdb->posts} SET post_content = {$replace_expr} WHERE {$where_sql}", + "UPDATE {$wpdb->posts} SET post_content = {$replace_expr} WHERE post_type <> 'revision' AND ({$where_sql})", ...array_merge( $replace_args, $where_args ) ) ); From 0e35efe8b81e021db9099eefee391d08b5ebd58f Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 20 Mar 2026 22:28:18 +0100 Subject: [PATCH 6/6] Lint fix --- src/Media_Command.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Media_Command.php b/src/Media_Command.php index d53da53d..3dd7636a 100644 --- a/src/Media_Command.php +++ b/src/Media_Command.php @@ -1,6 +1,7 @@ make_copy( $file ); } - $name = Utils\basename( $file ); + $name = Path::basename( $file ); if ( Utils\get_flag_value( $assoc_args, 'preserve-filetime' ) ) { $file_time = @filemtime( $file ); @@ -407,7 +408,7 @@ public function import( $args, $assoc_args = array() ) { ++$errors; continue; } - $name = (string) strtok( Utils\basename( $file ), '?' ); + $name = (string) strtok( Path::basename( $file ), '?' ); } if ( ! empty( $assoc_args['file_name'] ) ) { @@ -453,7 +454,7 @@ public function import( $args, $assoc_args = array() ) { } if ( empty( $post_array['post_title'] ) ) { - $post_array['post_title'] = preg_replace( '/\.[^.]+$/', '', Utils\basename( $file ) ); + $post_array['post_title'] = preg_replace( '/\.[^.]+$/', '', Path::basename( $file ) ); } if ( Utils\get_flag_value( $assoc_args, 'skip-copy' ) ) { @@ -684,7 +685,7 @@ private function gcd( $num1, $num2 ) { */ private function make_copy( $path ) { $dir = get_temp_dir(); - $filename = Utils\basename( $path ); + $filename = Path::basename( $path ); if ( empty( $filename ) ) { $filename = (string) time(); }