Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a CLI migration command to update attachment file names #63

Closed
wants to merge 9 commits into from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/vendor
composer.lock
65 changes: 65 additions & 0 deletions inc/class-command.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php
roborourke marked this conversation as resolved.
Show resolved Hide resolved
/**
* CLI Commands for Tachyon.
*/

class Tachyon_Command extends WP_CLI_Command {

/**
* Update attachment file names to work with Tachyon.
*
* Certain file names that end in dimensions such as those produced by
* WordPress eg. example-150x150.jpg can cause problems when uploaded
* as an original image. This prevents Tachyon from accurately and
* performantly rewriting the post content.
*
* @subcommand migrate-files
* @synopsis [--network] [--remove-old-files] [--sites-page=<int>]
*/
public function migrate_files( $args, $assoc_args ) {
global $wpdb;

$assoc_args = wp_parse_args( $assoc_args, [
'network' => false,
'remove-old-files' => false,
'sites-page' => 0
] );

$sites = [ get_current_blog_id() ];
if ( $assoc_args['network'] ) {
$sites = get_sites( [
'fields' => 'ids',
'offset' => $assoc_args['sites-page'],
] );
}

foreach ( $sites as $site_id ) {
switch_to_blog( $site_id );

if ( $assoc_args['network'] ) {
WP_CLI::log( "Processing site {$site_id}" );
}

$attachments = $wpdb->get_col( "SELECT post_id
FROM {$wpdb->postmeta}
WHERE meta_key = '_wp_attached_file'
AND meta_value REGEXP '-[[:digit:]]+x[[:digit:]]+\.(jpe?g|png|gif)$';" );

WP_CLI::log( sprintf( 'Renaming %d attachments', count( $attachments ) ) );

foreach ( $attachments as $attachment_id ) {
$result = Tachyon::_rename_file( $attachment_id, $assoc_args['remove-old-files'] );
if ( $result ) {
WP_CLI::log( sprintf( 'Renamed attachment %d successfully', $attachment_id ) );
} else {
WP_CLI::error( $result );
}
}

restore_current_blog();
}

WP_CLI::success( 'Done!' );
roborourke marked this conversation as resolved.
Show resolved Hide resolved
}

}
74 changes: 74 additions & 0 deletions inc/class-tachyon.php
Original file line number Diff line number Diff line change
Expand Up @@ -956,4 +956,78 @@ public function cleanup_rest_image_downsize( $response ) {
public function _override_image_downsize_in_rest_edit_context() {
return true;
}

/**
* Rename an existing attachment file to support Tachyon.
*
* Some legacy uploads may have image dimensions in the file name. Tachyon
* does not support this for performance reasons. This function renames
* attachments
*
* @param integer $attachment_id The attachment post ID.
* @param bool $remove_old_files If true deletes the old files.
* @return true|WP_Error
*/
public static function _rename_file( int $attachment_id, bool $remove_old_files = true ) {
global $wpdb;

$file = get_attached_file( $attachment_id );

// Trim dimensions from file name and make sure it's actually different.
$new_file = preg_replace( '/-\d+x\d+\.(jpe?g|png|gif)$/', '.$1', $file );
if ( $file === $new_file ) {
return new WP_Error( 'tachyon_rename_file', "{$file} does not need to be renamed" );
}

// Make sure it's unique.
$new_file = wp_unique_filename( dirname( $file ), basename( $new_file ) );
$new_file = dirname( $file ) . DIRECTORY_SEPARATOR . $new_file;

// Copy old file to new name.
$copied = copy( $file, $new_file );
if ( ! $copied ) {
return new WP_Error( 'tachyon_rename_file', "{$file} could not be copied to {$new_file}" );
}

// Generate new metadata.
$metadata = wp_generate_attachment_metadata( $attachment_id, $new_file );
if ( $metadata ) {
wp_update_attachment_metadata( $attachment_id, $metadata );
update_attached_file( $attachment_id, $new_file );
clean_attachment_cache( $attachment_id );

// Update posts table content.
$wpdb->query( 'SET @uids := 0;' );
$wpdb->query( $wpdb->prepare(
"UPDATE {$wpdb->posts}
SET post_content = REPLACE(post_content, %s, %s)
WHERE post_content LIKE %s
AND ( SELECT @uids := CONCAT_WS(',', ID, @uids) );",
_wp_relative_upload_path( $file ),
_wp_relative_upload_path( $new_file ),
'%' . $wpdb->esc_like( _wp_relative_upload_path( $file ) ) . '%'
) );
$updated_ids = $wpdb->get_var( 'SELECT @uids;' );

// Refresh post caches.
if ( $updated_ids ) {
$updated_ids = array_map( 'absint', explode( ',', $updated_ids ) );
$updated_ids = array_filter( $updated_ids );
array_map( 'clean_post_cache', $updated_ids );
}

// Optionally find and remove the old files.
if ( $remove_old_files ) {
$pattern = substr( $file, 0, strrpos( $file, '.' ) );
$old_files = glob( "$pattern*" );
foreach ( $old_files as $old_file ) {
unlink( $old_file );
}
}

return true;
} else {
return new WP_Error( 'tachyon_rename_file', "Could not generate new attachment metadata for attachment ID {$attachment_id}" );
}
}
}
4 changes: 4 additions & 0 deletions tachyon.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
}

require_once( dirname( __FILE__ ) . '/inc/class-tachyon.php' );
if ( defined( 'WP_CLI' ) && WP_CLI ) {
require_once( dirname( __FILE__ ) . '/inc/class-command.php' );
WP_CLI::add_command( 'tachyon', 'Tachyon_Command' );
}

Tachyon::instance();

Expand Down
2 changes: 1 addition & 1 deletion tests/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
// 2. Plugin installed inside of WordPress.org developer checkout
// 3. Tests checked out to /tmp
if ( false !== getenv( 'WP_DEVELOP_DIR' ) ) {
$test_root = getenv( 'WP_DEVELOP_DIR' ) . '/tests/phpunit';
$test_root = getenv( 'WP_DEVELOP_DIR' );
} elseif ( file_exists( '../../../../tests/phpunit/includes/bootstrap.php' ) ) {
$test_root = '../../../../tests/phpunit';
} elseif ( file_exists( '/tmp/wordpress-tests-lib/includes/bootstrap.php' ) ) {
Expand Down
Binary file added tests/data/tachyon-1280x719.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
133 changes: 133 additions & 0 deletions tests/tests/class-cli.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php
namespace HM\Tachyon\Tests;

use ReflectionClass;
use Tachyon;
use WP_UnitTestCase;

/**
* Ensure the tachyon plugin updates gallery and image links.
*
* @ticket 48
*/
class Tests_CLI extends WP_UnitTestCase {

/**
* @var int[] Attachment IDs
*/
static $attachment_ids;

/**
* @var array[] Original array of image sizes.
*/
static $wp_additional_image_sizes;

/**
* Set up attachments and posts require for testing.
*
* tachyon-1280x719.jpg: 1280x719
* Photo by Digital Buggu from Pexels
* @link https://www.pexels.com/photo/0-7-rpm-171195/
*/
static public function wpSetUpBeforeClass( $factory ) {
global $_wp_additional_image_sizes;
self::$wp_additional_image_sizes = $_wp_additional_image_sizes;

// Ensure pre WP 5.3.1 behaviour with image file having dimensions in file name.
add_filter( 'wp_unique_filename', __NAMESPACE__ . '\\Tests_CLI::unique_filename_override' );

self::$attachment_ids['tachyon-1280x719'] = $factory->attachment->create_upload_object(
realpath( __DIR__ . '/../data/tachyon-1280x719.jpg')
);

remove_filter( 'wp_unique_filename', __NAMESPACE__ . '\\Tests_CLI::unique_filename_override' );
}

/**
* Runs the routine after all tests have been run.
*
* This deletes the files from the uploads directory
* to account for the test suite returning the posts
* table to the original state.
*/
public static function wpTearDownAfterClass() {
global $_wp_additional_image_sizes;
$_wp_additional_image_sizes = self::$wp_additional_image_sizes;

$singleton = Tachyon::instance(); // Get Tachyon instance.
$reflection = new ReflectionClass( $singleton );
$instance = $reflection->getProperty( 'image_sizes' );
$instance->setAccessible( true ); // Allow modification of image sizes.
$instance->setValue( null, null ); // Reset image sizes for next tests.
$instance->setAccessible( false ); // clean up.

$uploads_dir = wp_upload_dir()['basedir'];

$files = glob( $uploads_dir . '/*' );
array_walk( $files, function ( $file ) {
if ( is_file( $file ) ) {
unlink($file);
}
} );
rmdir( $uploads_dir );
}

/**
* Prevents WP from fixing the file name during upload.
*
* This occurs if the file name contains dimensions as a suffix.
* This is to help test for backwards compat with WP 5.3.0 and earlier.
*
* @param string $filename
* @return string
*/
public static function unique_filename_override( $filename ) {
if ( strpos( $filename, 'tachyon-1280x719' ) === false ) {
return $filename;
}

return str_replace( '-1.jpg', '.jpg', $filename );
}

public function test_file_renaming() {
$attachment_id = self::$attachment_ids['tachyon-1280x719'];

// Create a post with a reference to the file.
$post_id = wp_insert_post( [
'post_title' => 'Replace test',
'post_content' => sprintf(
'<figure><img src="%s" alt="" /></figure>',
wp_get_attachment_image_url( $attachment_id, 'full' )
),
'post_status' => 'publish',
] );

// Get the file path.
$file = get_attached_file( $attachment_id );
$thumb_file = dirname( $file ) . '/' . basename( $file, '.jpg' ) . '-150x150.jpg';

// Confirm original file name.
$this->assertEquals( 'tachyon-1280x719.jpg', basename( $file ) );
// Confirm original exists.
$this->assertTrue( file_exists( $file ), "File $file exists" );
// Confirm a thumbnail exists.
$this->assertTrue( file_exists( $thumb_file ), "Thumbnail $thumb_file exists" );

// Rename the attachment.
$result = Tachyon::_rename_file( $attachment_id );
$this->assertTrue( $result, 'Attachment renamed successfully' );

$new_file = get_attached_file( $attachment_id );

// Confirm new file name.
$this->assertEquals( 'tachyon-1.jpg', basename( $new_file ) );
// Confirm old original has been removed.
$this->assertFalse( file_exists( $file ), "File $file deleted" );
// Confirm old thumbnail has been removed.
$this->assertFalse( file_exists( $thumb_file ), "Thumbnail $file deleted" );

// Confirm post content has been updated.
$post = get_post( $post_id );
$this->assertContains( 'tachyon-1.jpg', $post->post_content );
}
}