Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions features/media-regenerate.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
<?php
add_action( 'after_setup_theme', function(){
add_image_size( 'test1', 400, 400, true );
});
"""
And I run `wp option update uploads_use_yearmonth_folders 0`

When I run `wp media import {CACHE_DIR}/canola.jpg --title="My imported attachment" --porcelain`
Then save STDOUT as {ATTACHMENT_ID}
And the wp-content/uploads/canola-400x400.jpg file should exist

# Get the full URL of the test1 thumbnail.
When I run `wp eval "echo wp_get_attachment_image_src( {ATTACHMENT_ID}, 'test1' )[0];"`
Then save STDOUT as {OLD_THUMBNAIL_URL}

# Create a post referencing the old thumbnail URL in post content.
When I run `wp post create --post_title="Test Post" --post_status=publish --post_content="{OLD_THUMBNAIL_URL}" --porcelain`
Then save STDOUT as {POST_ID}

# Confirm the old URL is in post content before regeneration.
When I run `wp post get {POST_ID} --field=post_content`
Then STDOUT should contain:
"""
canola-400x400.jpg
"""

# Change "test1" image size dimensions.
Given a wp-content/mu-plugins/media-settings.php file:
"""
<?php
add_action( 'after_setup_theme', function(){
add_image_size( 'test1', 350, 350, true );
});
"""

# Regenerate "test1" without --update-attachment-refs - post content should be unchanged.
When I run `wp media regenerate {ATTACHMENT_ID} --image_size=test1 --yes`
Then STDOUT should contain:
"""
1/1 Regenerated "test1" thumbnail for "My imported attachment"
"""
And the wp-content/uploads/canola-350x350.jpg file should exist

When I run `wp post get {POST_ID} --field=post_content`
Then STDOUT should contain:
"""
canola-400x400.jpg
"""

# Change "test1" back to 400x400 so we can test --update-attachment-refs.
Given a wp-content/mu-plugins/media-settings.php file:
"""
<?php
add_action( 'after_setup_theme', function(){
add_image_size( 'test1', 400, 400, true );
});
"""
When I run `wp media regenerate {ATTACHMENT_ID} --image_size=test1 --yes`
Then STDOUT should contain:
"""
1/1 Regenerated "test1" thumbnail for "My imported attachment"
"""
And the wp-content/uploads/canola-400x400.jpg file should exist

# Change "test1" to 350x350 and regenerate with --update-attachment-refs.
Given a wp-content/mu-plugins/media-settings.php file:
"""
<?php
add_action( 'after_setup_theme', function(){
add_image_size( 'test1', 350, 350, true );
});
"""
When I run `wp media regenerate {ATTACHMENT_ID} --image_size=test1 --update-attachment-refs --yes`
Then STDOUT should contain:
"""
1/1 Regenerated "test1" thumbnail for "My imported attachment"
"""
And STDOUT should contain:
"""
Success: Regenerated 1 of 1 images.
"""
And the wp-content/uploads/canola-350x350.jpg file should exist

# Confirm the post content was updated to use the new thumbnail URL.
When I run `wp post get {POST_ID} --field=post_content`
Then STDOUT should contain:
"""
canola-350x350.jpg
"""
And STDOUT should not contain:
"""
canola-400x400.jpg
"""
122 changes: 115 additions & 7 deletions src/Media_Command.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use WP_CLI\Utils;
use WP_CLI\Path;

/**
* Imports files as attachments, regenerates thumbnails, or lists registered image sizes.
Expand Down Expand Up @@ -74,6 +75,9 @@ class Media_Command extends WP_CLI_Command {
* [--delete-unknown]
* : Only delete thumbnails for old unregistered image sizes.
*
* [--update-attachment-refs]
* : Update references to regenerated thumbnails in post content.
*
* [--yes]
* : Answer yes to the confirmation message. Confirmation only shows when no IDs passed as arguments.
*
Expand Down Expand Up @@ -123,7 +127,7 @@ class Media_Command extends WP_CLI_Command {
* Success: Regenerated 3 of 3 images.
*
* @param string[] $args Positional arguments.
* @param array{image_size?: string|string[], 'skip-delete'?: bool, 'only-missing'?: bool, 'delete-unknown'?: bool, yes?: bool} $assoc_args Associative arguments.
* @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.
* @return void
*/
public function regenerate( $args, $assoc_args = array() ) {
Expand Down Expand Up @@ -172,6 +176,8 @@ public function regenerate( $args, $assoc_args = array() ) {
$skip_delete = false;
}

$update_attachment_refs = Utils\get_flag_value( $assoc_args, 'update-attachment-refs' );

$additional_mime_types = array();

if ( Utils\wp_version_compare( '4.7', '>=' ) ) {
Expand Down Expand Up @@ -212,7 +218,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 ) ) {
Expand Down Expand Up @@ -384,7 +390,7 @@ public function import( $args, $assoc_args = array() ) {
} else {
$tempfile = $this->make_copy( $file );
}
$name = Utils\basename( $file );
$name = Path::basename( $file );

if ( Utils\get_flag_value( $assoc_args, 'preserve-filetime' ) ) {
$file_time = @filemtime( $file );
Expand All @@ -402,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'] ) ) {
Expand Down Expand Up @@ -448,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' ) ) {
Expand Down Expand Up @@ -679,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();
}
Expand Down Expand Up @@ -715,6 +721,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
Expand All @@ -724,7 +731,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 ) {
Expand Down Expand Up @@ -752,6 +759,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 );

Expand Down Expand Up @@ -846,6 +867,35 @@ 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<string, array<string, mixed>> $new_sizes
*/
$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'] ) ) {
continue;
}
/**
* @var array{file: string} $size_data
*/
$new_url = $dir_url . $size_data['file'];
if ( $old_url !== $new_url ) {
$url_replacements[ $old_url ] = $new_url;
}
}
Comment thread
swissspidy marked this conversation as resolved.
if ( ! empty( $url_replacements ) ) {
$this->update_post_content_for_attachment( $url_replacements );
}
}
}

++$successes;
}

Expand Down Expand Up @@ -1762,4 +1812,62 @@ private function delete_unknown_image_sizes( $id, $fullsizepath ) {
// @phpstan-ignore argument.type
wp_update_attachment_metadata( $id, $original_meta );
}

/**
* 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 array<string, string> $url_replacements Map of old URL => new URL.
* @return void
*/
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 );

// 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 post_type <> 'revision' AND ({$where_sql})",
...$where_args
)
);

$result = $wpdb->query(
$wpdb->prepare(
"UPDATE {$wpdb->posts} SET post_content = {$replace_expr} WHERE post_type <> 'revision' AND ({$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 {
Comment thread
swissspidy marked this conversation as resolved.
if ( ! empty( $post_ids ) ) {
foreach ( $post_ids as $post_id ) {
clean_post_cache( (int) $post_id );
}
}
wp_cache_set( 'last_changed', microtime(), 'posts' );
}
}
}