Skip to content

Commit 1d86009

Browse files
CopilotswissspidyCopilot
authored
Add --update-attachment-refs flag to wp media regenerate (#242)
Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Pascal Birchler <pascal.birchler@gmail.com> Co-authored-by: Pascal Birchler <pascalb@google.com>
1 parent 0272327 commit 1d86009

2 files changed

Lines changed: 215 additions & 7 deletions

File tree

features/media-regenerate.feature

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1933,3 +1933,103 @@ Feature: Regenerate WordPress attachments
19331933
"""
19341934
site_icon-270
19351935
"""
1936+
1937+
Scenario: Update post content references when regenerating a specific image size
1938+
Given download:
1939+
| path | url |
1940+
| {CACHE_DIR}/canola.jpg | http://wp-cli.org/behat-data/canola.jpg |
1941+
And a wp-content/mu-plugins/media-settings.php file:
1942+
"""
1943+
<?php
1944+
add_action( 'after_setup_theme', function(){
1945+
add_image_size( 'test1', 400, 400, true );
1946+
});
1947+
"""
1948+
And I run `wp option update uploads_use_yearmonth_folders 0`
1949+
1950+
When I run `wp media import {CACHE_DIR}/canola.jpg --title="My imported attachment" --porcelain`
1951+
Then save STDOUT as {ATTACHMENT_ID}
1952+
And the wp-content/uploads/canola-400x400.jpg file should exist
1953+
1954+
# Get the full URL of the test1 thumbnail.
1955+
When I run `wp eval "echo wp_get_attachment_image_src( {ATTACHMENT_ID}, 'test1' )[0];"`
1956+
Then save STDOUT as {OLD_THUMBNAIL_URL}
1957+
1958+
# Create a post referencing the old thumbnail URL in post content.
1959+
When I run `wp post create --post_title="Test Post" --post_status=publish --post_content="{OLD_THUMBNAIL_URL}" --porcelain`
1960+
Then save STDOUT as {POST_ID}
1961+
1962+
# Confirm the old URL is in post content before regeneration.
1963+
When I run `wp post get {POST_ID} --field=post_content`
1964+
Then STDOUT should contain:
1965+
"""
1966+
canola-400x400.jpg
1967+
"""
1968+
1969+
# Change "test1" image size dimensions.
1970+
Given a wp-content/mu-plugins/media-settings.php file:
1971+
"""
1972+
<?php
1973+
add_action( 'after_setup_theme', function(){
1974+
add_image_size( 'test1', 350, 350, true );
1975+
});
1976+
"""
1977+
1978+
# Regenerate "test1" without --update-attachment-refs - post content should be unchanged.
1979+
When I run `wp media regenerate {ATTACHMENT_ID} --image_size=test1 --yes`
1980+
Then STDOUT should contain:
1981+
"""
1982+
1/1 Regenerated "test1" thumbnail for "My imported attachment"
1983+
"""
1984+
And the wp-content/uploads/canola-350x350.jpg file should exist
1985+
1986+
When I run `wp post get {POST_ID} --field=post_content`
1987+
Then STDOUT should contain:
1988+
"""
1989+
canola-400x400.jpg
1990+
"""
1991+
1992+
# Change "test1" back to 400x400 so we can test --update-attachment-refs.
1993+
Given a wp-content/mu-plugins/media-settings.php file:
1994+
"""
1995+
<?php
1996+
add_action( 'after_setup_theme', function(){
1997+
add_image_size( 'test1', 400, 400, true );
1998+
});
1999+
"""
2000+
When I run `wp media regenerate {ATTACHMENT_ID} --image_size=test1 --yes`
2001+
Then STDOUT should contain:
2002+
"""
2003+
1/1 Regenerated "test1" thumbnail for "My imported attachment"
2004+
"""
2005+
And the wp-content/uploads/canola-400x400.jpg file should exist
2006+
2007+
# Change "test1" to 350x350 and regenerate with --update-attachment-refs.
2008+
Given a wp-content/mu-plugins/media-settings.php file:
2009+
"""
2010+
<?php
2011+
add_action( 'after_setup_theme', function(){
2012+
add_image_size( 'test1', 350, 350, true );
2013+
});
2014+
"""
2015+
When I run `wp media regenerate {ATTACHMENT_ID} --image_size=test1 --update-attachment-refs --yes`
2016+
Then STDOUT should contain:
2017+
"""
2018+
1/1 Regenerated "test1" thumbnail for "My imported attachment"
2019+
"""
2020+
And STDOUT should contain:
2021+
"""
2022+
Success: Regenerated 1 of 1 images.
2023+
"""
2024+
And the wp-content/uploads/canola-350x350.jpg file should exist
2025+
2026+
# Confirm the post content was updated to use the new thumbnail URL.
2027+
When I run `wp post get {POST_ID} --field=post_content`
2028+
Then STDOUT should contain:
2029+
"""
2030+
canola-350x350.jpg
2031+
"""
2032+
And STDOUT should not contain:
2033+
"""
2034+
canola-400x400.jpg
2035+
"""

src/Media_Command.php

Lines changed: 115 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22

33
use WP_CLI\Utils;
4+
use WP_CLI\Path;
45

56
/**
67
* Imports files as attachments, regenerates thumbnails, or lists registered image sizes.
@@ -82,6 +83,9 @@ class Media_Command extends WP_CLI_Command {
8283
* [--delete-unknown]
8384
* : Only delete thumbnails for old unregistered image sizes.
8485
*
86+
* [--update-attachment-refs]
87+
* : Update references to regenerated thumbnails in post content.
88+
*
8589
* [--yes]
8690
* : Answer yes to the confirmation message. Confirmation only shows when no IDs passed as arguments.
8791
*
@@ -131,7 +135,7 @@ class Media_Command extends WP_CLI_Command {
131135
* Success: Regenerated 3 of 3 images.
132136
*
133137
* @param string[] $args Positional arguments.
134-
* @param array{image_size?: string|string[], 'skip-delete'?: bool, 'only-missing'?: bool, 'delete-unknown'?: bool, yes?: bool} $assoc_args Associative arguments.
138+
* @param array{image_size?: string|string[], 'skip-delete'?: bool, 'only-missing'?: bool, 'delete-unknown'?: bool, 'update-attachment-refs'?: bool, yes?: bool} $assoc_args Associative arguments.
135139
* @return void
136140
*/
137141
public function regenerate( $args, $assoc_args = array() ) {
@@ -180,6 +184,8 @@ public function regenerate( $args, $assoc_args = array() ) {
180184
$skip_delete = false;
181185
}
182186

187+
$update_attachment_refs = Utils\get_flag_value( $assoc_args, 'update-attachment-refs' );
188+
183189
$additional_mime_types = array();
184190

185191
if ( Utils\wp_version_compare( '4.7', '>=' ) ) {
@@ -220,7 +226,7 @@ public function regenerate( $args, $assoc_args = array() ) {
220226
// @phpstan-ignore function.deprecated
221227
Utils\wp_clear_object_cache();
222228
}
223-
$this->process_regeneration( $post_id, $skip_delete, $only_missing, $delete_unknown, $image_sizes, $number . '/' . $count, $successes, $errors, $skips );
229+
$this->process_regeneration( $post_id, $skip_delete, $only_missing, $delete_unknown, $image_sizes, $update_attachment_refs, $number . '/' . $count, $successes, $errors, $skips );
224230
}
225231

226232
if ( isset( $image_size_filters ) ) {
@@ -524,7 +530,7 @@ public function import( $args, $assoc_args = array() ) {
524530
} else {
525531
$tempfile = $this->make_copy( $file );
526532
}
527-
$name = Utils\basename( $file );
533+
$name = Path::basename( $file );
528534

529535
if ( Utils\get_flag_value( $assoc_args, 'preserve-filetime' ) ) {
530536
$file_time = @filemtime( $file );
@@ -542,7 +548,7 @@ public function import( $args, $assoc_args = array() ) {
542548
++$errors;
543549
continue;
544550
}
545-
$name = (string) strtok( Utils\basename( $file ), '?' );
551+
$name = (string) strtok( Path::basename( $file ), '?' );
546552
}
547553

548554
if ( ! empty( $assoc_args['file_name'] ) ) {
@@ -588,7 +594,7 @@ public function import( $args, $assoc_args = array() ) {
588594
}
589595

590596
if ( empty( $post_array['post_title'] ) ) {
591-
$post_array['post_title'] = preg_replace( '/\.[^.]+$/', '', Utils\basename( $file ) );
597+
$post_array['post_title'] = preg_replace( '/\.[^.]+$/', '', Path::basename( $file ) );
592598
}
593599

594600
if ( Utils\get_flag_value( $assoc_args, 'skip-copy' ) ) {
@@ -819,7 +825,7 @@ private function gcd( $num1, $num2 ) {
819825
*/
820826
private function make_copy( $path ) {
821827
$dir = get_temp_dir();
822-
$filename = Utils\basename( $path );
828+
$filename = Path::basename( $path );
823829
if ( empty( $filename ) ) {
824830
$filename = (string) time();
825831
}
@@ -855,6 +861,7 @@ private function get_image_sizes_description( array $sizes, $noun, $default_if_e
855861
* @param bool $only_missing
856862
* @param bool $delete_unknown
857863
* @param string[] $image_sizes
864+
* @param bool $update_attachment_refs
858865
* @param string $progress
859866
* @param int $successes
860867
* @param int $errors
@@ -864,7 +871,7 @@ private function get_image_sizes_description( array $sizes, $noun, $default_if_e
864871
* @param-out int $skips
865872
* @return void
866873
*/
867-
private function process_regeneration( $id, $skip_delete, $only_missing, $delete_unknown, $image_sizes, $progress, &$successes, &$errors, &$skips ) {
874+
private function process_regeneration( $id, $skip_delete, $only_missing, $delete_unknown, $image_sizes, $update_attachment_refs, $progress, &$successes, &$errors, &$skips ) {
868875

869876
$title = get_the_title( $id );
870877
if ( '' === $title ) {
@@ -892,6 +899,20 @@ private function process_regeneration( $id, $skip_delete, $only_missing, $delete
892899

893900
$original_meta = wp_get_attachment_metadata( $id );
894901

902+
$old_size_urls = array();
903+
if ( $update_attachment_refs && is_array( $original_meta ) && ! empty( $original_meta['sizes'] ) ) {
904+
$attachment_url = wp_get_attachment_url( $id );
905+
if ( $attachment_url ) {
906+
$dir_url = trailingslashit( dirname( $attachment_url ) );
907+
$sizes_to_track = $image_sizes ?: array_keys( $original_meta['sizes'] );
908+
foreach ( $sizes_to_track as $size ) {
909+
if ( ! empty( $original_meta['sizes'][ $size ]['file'] ) ) {
910+
$old_size_urls[ $size ] = $dir_url . $original_meta['sizes'][ $size ]['file'];
911+
}
912+
}
913+
}
914+
}
915+
895916
if ( $delete_unknown ) {
896917
$this->delete_unknown_image_sizes( $id, $fullsizepath );
897918

@@ -986,6 +1007,35 @@ private function process_regeneration( $id, $skip_delete, $only_missing, $delete
9861007

9871008
WP_CLI::log( "$progress Regenerated thumbnails for $att_desc." );
9881009
}
1010+
1011+
if ( $update_attachment_refs && ! empty( $old_size_urls ) && is_array( $metadata ) && ! empty( $metadata['sizes'] ) ) {
1012+
$attachment_url = wp_get_attachment_url( $id );
1013+
if ( $attachment_url ) {
1014+
$dir_url = trailingslashit( dirname( $attachment_url ) );
1015+
/**
1016+
* @var array<string, array<string, mixed>> $new_sizes
1017+
*/
1018+
$new_sizes = is_array( $metadata['sizes'] ) ? $metadata['sizes'] : array();
1019+
$url_replacements = array();
1020+
foreach ( $old_size_urls as $size => $old_url ) {
1021+
$size_data = $new_sizes[ $size ] ?? null;
1022+
if ( ! is_array( $size_data ) || empty( $size_data['file'] ) ) {
1023+
continue;
1024+
}
1025+
/**
1026+
* @var array{file: string} $size_data
1027+
*/
1028+
$new_url = $dir_url . $size_data['file'];
1029+
if ( $old_url !== $new_url ) {
1030+
$url_replacements[ $old_url ] = $new_url;
1031+
}
1032+
}
1033+
if ( ! empty( $url_replacements ) ) {
1034+
$this->update_post_content_for_attachment( $url_replacements );
1035+
}
1036+
}
1037+
}
1038+
9891039
++$successes;
9901040
}
9911041

@@ -2005,4 +2055,62 @@ private function delete_unknown_image_sizes( $id, $fullsizepath ) {
20052055
// @phpstan-ignore argument.type
20062056
wp_update_attachment_metadata( $id, $original_meta );
20072057
}
2058+
2059+
/**
2060+
* Updates post content replacing old attachment URLs with new ones in a single query.
2061+
*
2062+
* Applies all replacements as nested REPLACE() calls so only one table scan is needed.
2063+
*
2064+
* @param array<string, string> $url_replacements Map of old URL => new URL.
2065+
* @return void
2066+
*/
2067+
private function update_post_content_for_attachment( array $url_replacements ) {
2068+
global $wpdb;
2069+
2070+
if ( empty( $url_replacements ) ) {
2071+
return;
2072+
}
2073+
2074+
$replace_expr = 'post_content';
2075+
$replace_args = array();
2076+
$where_clauses = array();
2077+
$where_args = array();
2078+
2079+
foreach ( $url_replacements as $old_url => $new_url ) {
2080+
$replace_expr = "REPLACE($replace_expr, %s, %s)";
2081+
$replace_args[] = $old_url;
2082+
$replace_args[] = $new_url;
2083+
$where_clauses[] = 'post_content LIKE %s';
2084+
$where_args[] = '%' . $wpdb->esc_like( $old_url ) . '%';
2085+
}
2086+
2087+
$where_sql = implode( ' OR ', $where_clauses );
2088+
2089+
// First, find the IDs of posts whose content will be updated so we can clear their object cache entries.
2090+
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
2091+
$post_ids = $wpdb->get_col(
2092+
$wpdb->prepare(
2093+
"SELECT ID FROM {$wpdb->posts} WHERE post_type <> 'revision' AND ({$where_sql})",
2094+
...$where_args
2095+
)
2096+
);
2097+
2098+
$result = $wpdb->query(
2099+
$wpdb->prepare(
2100+
"UPDATE {$wpdb->posts} SET post_content = {$replace_expr} WHERE post_type <> 'revision' AND ({$where_sql})",
2101+
...array_merge( $replace_args, $where_args )
2102+
)
2103+
);
2104+
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
2105+
if ( false === $result ) {
2106+
WP_CLI::warning( 'Failed to update post content references for attachment.' );
2107+
} else {
2108+
if ( ! empty( $post_ids ) ) {
2109+
foreach ( $post_ids as $post_id ) {
2110+
clean_post_cache( (int) $post_id );
2111+
}
2112+
}
2113+
wp_cache_set( 'last_changed', microtime(), 'posts' );
2114+
}
2115+
}
20082116
}

0 commit comments

Comments
 (0)