diff --git a/inc/class-img-shortcode-data-migration.php b/inc/class-img-shortcode-data-migration.php index f1fbd47..7be08d5 100644 --- a/inc/class-img-shortcode-data-migration.php +++ b/inc/class-img-shortcode-data-migration.php @@ -7,38 +7,52 @@ class Img_Shortcode_Data_Migration { - private static function img_shortcode_regex() { - $img_shortcode_regex = - '(?:]+' . - 'href="(?P[^"]*)"' . - '[^>]*>)?' . - ']*' . - 'class="' . - '(?|size-(?P\w+))?' . ' ?' . - '(?|wp-image-(?P\d+))?' . ' ?' . - '(?|(?Palign[\w-]+))?' . ' ?' . - '[^"]*" ' . // end of class attribute - 'src="(?P[^"]*)" ?' . - '(?:alt="(?P[^"]*)" ?)?' . - '(?:width="(?P[^"]*)" ?)?' . - '(?:height="(?P[^"]*)" ?)?' . - '[^>]*>' . - '(?:<\/a>)?'; - - return $img_shortcode_regex; + public static function img_tag_regex() { + $img_tag_regex = '(?:)?' . + '\d*)' . + ')?' . + '(?:' . + '(\s)?' . + '(?P[^"]*)' . + ')?' . + '")?' . + ' \/>' . + '(?:<\/a>)?'; + return $img_tag_regex; } - - private static function caption_shortcode_regex() { + public static function caption_shortcode_regex() { $caption_shortcode_regex = - '\[caption' . - '[^\]]*' . '\]\]?' . - self::img_shortcode_regex() . - '(?: (?P[^\]]*))' . - '\[\[?\/caption\]\]?'; + '\[caption[^\]]*\]' . + self::img_tag_regex() . + '(?P[^\[]*)' . + '\[\/caption]'; return $caption_shortcode_regex; } + private static function img_shortcode_regex() { + $img_shortcode_regex = '\[img ' . '[^\]]*]'; + return $img_shortcode_regex; + } + public static function find_img_tags_for_replacement_on_post( $post ) { if ( ! $post = self::maybe_get_post_from_id( $post ) ) { return false; @@ -57,10 +71,10 @@ public static function find_img_tags_for_replacement( $post_content ) { $replacements = array(); - $img_shortcode_regex = self::img_shortcode_regex(); + $img_tag_regex = self::img_tag_regex(); preg_match_all( - "/$img_shortcode_regex/s", + "/$img_tag_regex/s", $post_content, $matches, PREG_SET_ORDER @@ -183,6 +197,104 @@ public static function convert_img_tag_to_shortcode( $img_tag, $attributes ) { } + /** + * Get all [img] shortcodes in a string for replacement. + * + * Used in converting data added by this plugin back to the default format + * of tags and [caption] shortcodes. + * + */ + public static function find_img_shortcodes_for_replacement( $post_content ) { + + $replacements = array(); + + $img_shortcode_regex = self::img_shortcode_regex(); + + preg_match_all( + "/$img_shortcode_regex/s", + $post_content, + $matches, + PREG_SET_ORDER + ); + + if ( 0 === count( $matches ) ) { + return array(); + } + + foreach ( $matches as $matched_pattern ) { + $replacements[ $matched_pattern[0] ] = self::convert_img_shortcode_to_tag( + $matched_pattern[0] + ); + } + + return $replacements; + + } + + + /** + * Convert an [img] shortcode as inserted by this plugin to the WP default + * representation. + * + * @param string `[img]` shortcode element + * @return string A `[caption]` shortcode element, or an tag + */ + public static function convert_img_shortcode_to_tag( $img_shortcode ) { + $atts = shortcode_parse_atts( $img_shortcode ); + + $caption = isset( $atts['caption'] ) ? $atts['caption'] : ''; + unset( $atts['caption'] ); + + $width = isset( $atts['width'] ) ? $atts['width'] : null; + + $align = isset( $atts['align'] ) ? $atts['align'] : 'alignnone'; + + // Use a size if set. + // If valid attachment, full is ok + // If not valid, use medium so we can provide width + if ( isset( $atts['size'] ) ) { + $size = $atts['size']; + } else { + if ( isset( $atts['attachment'] ) && get_permalink( $atts['attachment'] ) ) { + $size = $atts['size'] = 'full'; + } else { + $size = $atts['size'] = 'large'; + } + } + + $content = Img_Shortcode::callback( $atts ); + + if ( ! isset( $width ) && isset( $atts['attachment'] ) && get_permalink( $atts['attachment'] ) ) { + $attachment = wp_get_attachment_image_src( + (int) $atts['attachment'], $size + ); + $width = intval( $attachment[1] ); + } else { + /* If there's no width set and no valid attachment to get full/custom size dimensions from, fallback to large width */ + $width = '600'; + } + + if ( isset( $atts['attachment'] ) && $caption ) { + $id_string = 'id="attachment_' . esc_attr( sanitize_html_class( $atts['attachment'] ) ). '" '; + } else { + $id_string = ''; + } + + if ( $caption ) { + $content = + '[caption ' . + $id_string . + 'width="' . $width . '" ' . + 'align="' . $align . '"' . + ']' . + $content . + $caption . + '[/caption]'; + } + + return $content; + } + /** * Get a post from a Post ID or post object. * diff --git a/inc/class-img-shortcode.php b/inc/class-img-shortcode.php index b81f623..63ba841 100644 --- a/inc/class-img-shortcode.php +++ b/inc/class-img-shortcode.php @@ -150,6 +150,7 @@ public static function reversal( $content ) { * * @param array $attrs Shortcode attributes. See definitions in * @function `get_shortcode_ui_args()` + * * @return string */ public static function callback( $attr ) { @@ -190,7 +191,12 @@ public static function callback( $attr ) { } else if ( ! empty( $attr['src'] ) ) { $image_attr['src'] = esc_url( $attr['src'] ); } else { - return; // An image without a src isn't much of an image + $image_attr['src'] = ''; + } + + // If the attachment is invalid, store it in an `data-shortcode-attachment` attr for posterity + if ( '' === $image_attr['src'] && isset( $attr['attachment'] ) ) { + $image_attr['data-shortcode-attachment'] = $attr['attachment']; } foreach ( $image_attr as $attr_name => $attr_value ) { @@ -225,6 +231,7 @@ public static function callback( $attr ) { $image_html = apply_filters( 'img_shortcode_output_after_linkify', $image_html, $attr ); // If a caption is specified, wrap the image in the appropriat caption markup. + // When migrating, captioning is handled in the migration script if ( ! empty( $attr['caption'] ) ) { // The WP caption element requires a width defined @@ -259,15 +266,19 @@ public static function callback( $attr ) { */ private static function linkify( $img_tag, $attributes ) { - $_id = intval( $attributes['attachment'] ); + if ( isset( $attributes['attachment'] ) && get_permalink( $attributes['attachment'] ) ) { + $_id = intval( $attributes['attachment'] ); + } else { + $_id = false; + } $link_attrs = array(); if ( isset( $attributes['url'] ) ) { $link_attrs['href'] = esc_url( $attributes['url'] ); - } else if ( ! empty( $attributes['linkto'] ) && 'attachment' === $attributes['linkto'] ) { + } else if ( ! empty( $attributes['linkto'] ) && 'attachment' === $attributes['linkto'] && $_id ) { $link_attrs['href'] = get_permalink( $_id ); - } elseif ( ! empty( $attributes['linkto'] ) && 'file' === $attributes['linkto'] ) { + } elseif ( ! empty( $attributes['linkto'] ) && 'file' === $attributes['linkto'] && $_id ) { $attachment_src = wp_get_attachment_image_src( $_id, 'full', false, $attributes ); $link_attrs['href'] = $attachment_src[0]; } else { diff --git a/inc/class-wp-cli-img-shortcode-command.php b/inc/class-wp-cli-img-shortcode-command.php index 2466886..b33bf20 100644 --- a/inc/class-wp-cli-img-shortcode-command.php +++ b/inc/class-wp-cli-img-shortcode-command.php @@ -25,7 +25,7 @@ public function __construct() { * ## EXAMPLES * * ## Migrate all Posts to the Image Shortcake syntax - * wp image-shortcake-shortcode migrate `wp post list --post_type=post` --ids` + * wp image-shortcake migrate `wp post list --post_type=post` --ids` * * ## Converts images to shortcodes on one post, preserving a log to rollback in case of errors. * wp image-shortcake migrate 123 > potential-oops.txt @@ -102,6 +102,86 @@ public function migrate( $args, $assoc_args ) { } + + /** + * Revert post content from image shortcodes back to tags and [caption] shortcodes + * + * ## OPTIONS + * + * ... + * : One or more IDs of posts to update. + * + * [--dry-run] + * : Only show the content which is to be changed, don't update posts. + * + * ## EXAMPLES + * + * ## Revert all Posts from the Image Shortcake syntax + * wp image-shortcake revert `wp post list --post_type=post` --ids` + * + * ## Converts shortcodes back to images on one post, preserving a log to rollback in case of errors. + * wp image-shortcake revert 123 > potential-oops.txt + * + * @synopsis ... [--dry-run] + */ + public function revert( $args, $assoc_args ) { + + foreach ( array_filter( $args ) as $post_ID ) { + + $post = $this->fetcher->get_check( $post_ID ); + + $_content = $post->post_content; + + $replacements = Img_Shortcode_Data_Migration::find_img_shortcodes_for_replacement( $_content ); + + $_content = str_replace( + array_keys( $replacements ), + array_values( $replacements ), + $_content + ); + + WP_CLI::log( '' ); + + if ( 0 === count( $replacements ) ) { + WP_CLI::log( 'Nothing to replace on post ' . $post->ID . '. Skipping.' ); + WP_CLI::log( '' ); + continue; + } + + $header = 'Image shortcode replacements for post ' . $post->ID; + + WP_CLI::log( $header ); + WP_CLI::log( str_repeat( '=', strlen( $header ) ) ); + WP_CLI::log( '' ); + + foreach ( $replacements as $del => $ins ) { + \WP_CLI::log( \cli\Colors::colorize( '%C-%n' ) . $del, true ); + \WP_CLI::log( \cli\Colors::colorize( '%G+%n' ) . $ins, true ); + } + + WP_CLI::log( '' ); + + if ( isset( $assoc_args['dry-run'] ) ) { + WP_CLI::log( 'Post not updated: --dry-run specifed.' ); + WP_CLI::log( '' ); + continue; + } + + global $wpdb; + + // @codingStandardsIgnoreStart + $updated = $wpdb->update( $wpdb->posts, array( 'post_content' => $_content ), array( 'ID' => $post_ID ) ); + // @codingStandardsIgnoreEnd + + if ( 1 === $updated ) { + clean_post_cache( $post ); + WP_CLI::success( 'Updated post ' . $post->ID . '.' ); + } else { + WP_CLI::warning( 'There was an unexpected error updating post ' . $post->ID . '.' ); + } + } + } + } diff --git a/tests/test-img-shortcode-data-migration.php b/tests/test-img-shortcode-data-migration.php index 6d5aa3e..366253d 100644 --- a/tests/test-img-shortcode-data-migration.php +++ b/tests/test-img-shortcode-data-migration.php @@ -32,19 +32,49 @@ public function setUp() { $this->image_path = $upload_dir['path'] . '/fusion_image_placeholder_16x9_h2000.png'; $this->image_tag_from_attachment = - ''; + 'width="1024" height="540" ' . + 'class="size-large wp-image-' . $this->attachment_id . ' aligncenter" />'; $this->image_tag_from_src = '' . - '' . + 'width="1024" height="540" ' . + 'class="aligncenter" />' . ''; + $this->regex_test_1 = + '[caption id="attachment_9" align="alignnone" width="2448"]'. + '' . + 'Alt text' . + ' Caption text[/caption]'; + + $this->regex_test_2 = + '[caption id="attachment_9" align="alignleft" width="300"]' . + '' . + ' Caption text[/caption]'; + + $this->regex_test_3 = + '[caption id="attachment_9" align="alignright" width="660"]' . + '' . + ' Caption text[/caption]'; + + $this->regex_test_4 = + '' . + ''; + + $this->regex_test_5 = + ''; + + $this->caption_regex = Img_Shortcode_Data_Migration::caption_shortcode_regex(); + $this->img_regex = Img_Shortcode_Data_Migration::img_tag_regex(); } public function tearDown() { @@ -55,6 +85,48 @@ public function tearDown() { // @codingStandardsIgnoreEnd + /** + * Test our regex functions directly + * + */ + function test_img_caption_regexes() { + + $regex_test_1_matches = preg_match( + "/$this->caption_regex/s", + $this->regex_test_1, + $matches + ); + $this->assertEquals( 1, $regex_test_1_matches ); + + $regex_test_2_matches = preg_match( + "/$this->caption_regex/s", + $this->regex_test_2, + $matches + ); + $this->assertEquals( 1, $regex_test_2_matches ); + + $regex_test_3_matches = preg_match( + "/$this->caption_regex/s", + $this->regex_test_3, + $matches + ); + $this->assertEquals( 1, $regex_test_3_matches ); + + $regex_test_4_matches = preg_match( + "/$this->img_regex/s", + $this->regex_test_4, + $matches + ); + $this->assertEquals( 1, $regex_test_4_matches ); + + $regex_test_5_matches = preg_match( + "/$this->img_regex/s", + $this->regex_test_5, + $matches + ); + $this->assertEquals( 1, $regex_test_5_matches ); + } + /** * Case: tags where the src is an attachment * @@ -72,6 +144,104 @@ function test_img_tag_from_attachment() { $this->assertNotContains( 'src="', $replacements[ $img_tag ] ); } + /** + * Case: [img] shortcode conversion to + * + */ + function test_img_shortcode_conversion_to_img() { + + $attachment_id = $this->attachment_id; + $upload_dir = wp_upload_dir(); + $expected_src_attr = $upload_dir['url'] . '/fusion_image_placeholder_16x9_h2000.png'; + + // Test vanilla shortcode + $shortcode = '[img attachment="' . $attachment_id . '" /]'; + $conversion = Img_Shortcode_Data_Migration::convert_img_shortcode_to_tag( $shortcode ); + $this->assertContains( '' , $conversion ); + + // Test link href: linkto="file" + $shortcode = '[img attachment="' . $attachment_id . '" linkto="file" /]'; + $conversion = Img_Shortcode_Data_Migration::convert_img_shortcode_to_tag( $shortcode ); + $this->assertContains( 'href="' . $expected_src_attr . '"', $conversion ); + + // Test link href: linkto="attachment" + $shortcode = '[img attachment="' . $attachment_id . '" linkto="attachment" /]'; + $conversion = Img_Shortcode_Data_Migration::convert_img_shortcode_to_tag( $shortcode ); + $expected_href_attr = get_permalink( $attachment_id ); + $this->assertContains( 'href="' . $expected_href_attr . '"', $conversion ); + + // Test caption attribute (it should always get a width) + $caption = <<caption". It should contain HTML and markup. +EOL; + $shortcode = '[img attachment="' . $attachment_id . '" caption="' . esc_attr( $caption ) . '" /]'; + $conversion = Img_Shortcode_Data_Migration::convert_img_shortcode_to_tag( $shortcode ); + $expected_caption = esc_html( $caption ); + $this->assertContains( '[caption id="attachment_' . $attachment_id . '" width="2000" align="alignnone"]', $conversion ); + $this->assertContains( $expected_caption . '[/caption]', $conversion ); + + // Test caption width with a different size image + $caption = <<caption". It should contain HTML and markup. +EOL; + $shortcode = '[img attachment="' . $attachment_id . '" caption="' . esc_attr( $caption ) . '" size="full" /]'; + $conversion = Img_Shortcode_Data_Migration::convert_img_shortcode_to_tag( $shortcode ); + $expected_caption = esc_html( $caption ); + $this->assertContains( '[caption id="attachment_' . $attachment_id . '" width="2000" align="alignnone"]', $conversion ); + $this->assertContains( $expected_caption . '[/caption]', $conversion ); + + // Test no attachment + $shortcode = '[img caption="' . esc_attr( $caption ) . '" /]'; + $conversion = Img_Shortcode_Data_Migration::convert_img_shortcode_to_tag( $shortcode ); + $expected = '[caption ' . + 'width="600" align="alignnone"]' . + $expected_caption . + '[/caption]'; + $this->assertContains( $expected, $conversion ); + + // Test invalid attachment + $shortcode = '[img attachment="9999999" caption="' . esc_attr( $caption ) . '" /]'; + $conversion = Img_Shortcode_Data_Migration::convert_img_shortcode_to_tag( $shortcode ); + $expected = '[caption id="attachment_9999999" ' . + 'width="600" align="alignnone"]' . + $expected_caption . + '[/caption]'; + $this->assertContains( $expected, $conversion ); + + // Test cadillac 1 + $shortcode = '[img attachment="' . $attachment_id . '" linkto="attachment" size="full" caption="' . esc_attr( $caption ) . '" align="alignright" /]'; + $conversion = Img_Shortcode_Data_Migration::convert_img_shortcode_to_tag( $shortcode ); + $expected = '[caption id="attachment_' . + $attachment_id . + '" width="2000" align="alignright"]' . + $expected_caption . + '[/caption]'; + + $this->assertContains( $expected, $conversion ); + + // Test cadillac 2 + $shortcode = '[img attachment="' . $attachment_id . '" linkto="attachment" size="medium" caption="' . esc_attr( $caption ) . '" align="alignnone" /]'; + $conversion = Img_Shortcode_Data_Migration::convert_img_shortcode_to_tag( $shortcode ); + $expected_src = wp_get_attachment_image_src( $attachment_id, 'medium' ); + $expected_src_attr = $expected_src[0]; + $expected = '[caption id="attachment_' . + $attachment_id . + '" width="300" align="alignnone"]' . + $expected_caption . + '[/caption]'; + + $this->assertContains( $expected, $conversion ); + + } + /** * Case: tags with an external src * @@ -79,10 +249,10 @@ function test_img_tag_from_attachment() { function test_img_tag_from_src() { $img_tag = '' . - '' . + 'width="1024" height="540" ' . + 'class="aligncenter" />' . ''; $post_id = wp_insert_post( array( 'post_content' => "\r\n\r\n$img_tag\r\nblah blah blah" ) ); @@ -139,11 +309,11 @@ public function test_img_tags_wrapped_in_links() { * */ public function test_replace_caption_shortcodes() { - $caption_no_link = '[caption]' . $this->image_tag_from_src . ' Caption of image without attachment[/caption]'; + $caption_no_link = '[caption]' . $this->image_tag_from_src . 'Caption of image without attachment[/caption]'; $caption_with_link = '[caption width="1024"]' . '' . $this->image_tag_from_attachment . '' . - ' Caption of image linked to attachment page' . + 'Caption of image linked to attachment page' . '[/caption]'; $post_content = "Post content.\r\n\r\n$caption_no_link\r\n\r\n$caption_with_link";