11<?php
22
33use 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