diff --git a/README.md b/README.md index 72d9fdf73..533970b24 100644 --- a/README.md +++ b/README.md @@ -4696,6 +4696,62 @@ or return code 1 if it does not. +### wp post convert-to-blocks + +Converts the classic (non-block) content of one or more posts to block markup. + +~~~ +wp post convert-to-blocks ... [--dry-run] +~~~ + +Uses the server-side block conversion provided by the Gutenberg plugin +(see https://github.com/WordPress/gutenberg/pull/82013) to turn classic +HTML content into serialized block markup. Markup that no block claims +is kept verbatim inside a Custom HTML block, so nothing is lost. Posts +that already contain blocks or have no content are skipped. + +The conversion does not sanitize the markup. Saving the converted content +applies the usual kses filtering for the current user context, so a run +without `--user` is filtered as an untrusted author would be. Run the +command with `--user=` to keep markup that requires the +`unfiltered_html` capability, such as iframes or scripts. + +Requires a Gutenberg build that provides `gutenberg_html_to_block_markup()`. + +**OPTIONS** + + ... + One or more IDs of posts to convert. + + [--dry-run] + Preview which posts would be converted, without saving any changes. + +**EXAMPLES** + + # Convert a single post. + $ wp post convert-to-blocks 123 + Converted post 123. + Success: Converted 1 of 1 posts. + + # Convert every post of a post type. + $ wp post list --post_type=post --format=ids | xargs wp post convert-to-blocks + Converted post 123. + Converted post 124. + Warning: Post 125 already contains blocks. + Success: Converted 2 of 3 posts (1 skipped). + + # Preview a conversion without saving. + $ wp post convert-to-blocks 123 --dry-run + Would convert post 123. + Success: Would convert 1 of 1 posts. + + # Run as an administrator to keep markup that requires unfiltered_html. + $ wp post convert-to-blocks 123 --user=admin + Converted post 123. + Success: Converted 1 of 1 posts. + + + ### wp post block Manages blocks within post content. diff --git a/composer.json b/composer.json index fcd353184..2a3265ca2 100644 --- a/composer.json +++ b/composer.json @@ -148,6 +148,7 @@ "post url-to-id", "post has-blocks", "post has-block", + "post convert-to-blocks", "post block", "post block clone", "post block count", diff --git a/features/post-convert-to-blocks.feature b/features/post-convert-to-blocks.feature new file mode 100644 index 000000000..7b1bff0d9 --- /dev/null +++ b/features/post-convert-to-blocks.feature @@ -0,0 +1,132 @@ +Feature: Convert classic post content to blocks + + # The conversion depends on the server-side block conversion from + # https://github.com/WordPress/gutenberg/pull/82013 (branch + # `try/13163-php-block-conversion`), which no released Gutenberg build + # provides yet. The scenarios tagged @broken below need a Gutenberg build + # of that PR. To run them locally, build the plugin ZIP from that branch, + # remove the @broken tags, and add the following step after the + # `Given a WP install` step in each scenario: + # + # And I run `wp plugin install /path/to/gutenberg.zip --activate` + + Scenario: Conversion requires the Gutenberg build with server-side block conversion + Given a WP install + + When I try `wp post convert-to-blocks 1` + Then STDERR should be: + """ + Error: Server-side block conversion is not available. Activate the Gutenberg plugin from https://github.com/WordPress/gutenberg/pull/82013. + """ + And STDOUT should be empty + And the return code should be 1 + + @broken + Scenario: Convert a classic post to blocks + Given a WP install + + When I run `wp post create --post_title='Classic post' --post_content='

Title

Text

' --porcelain` + Then save STDOUT as {POST_ID} + + When I run `wp post convert-to-blocks {POST_ID}` + Then STDOUT should be: + """ + Converted post {POST_ID}. + Success: Converted 1 of 1 posts. + """ + And STDERR should be empty + + When I run `wp post get {POST_ID} --field=post_content` + Then STDOUT should contain: + """ + + """ + And STDOUT should contain: + """ + + """ + + When I run `wp post has-blocks {POST_ID}` + Then STDOUT should be: + """ + Success: Post {POST_ID} contains blocks. + """ + + @broken + Scenario: Preview a conversion with --dry-run + Given a WP install + + When I run `wp post create --post_title='Classic post' --post_content='

Title

Text

' --porcelain` + Then save STDOUT as {POST_ID} + + When I run `wp post convert-to-blocks {POST_ID} --dry-run` + Then STDOUT should be: + """ + Would convert post {POST_ID}. + Success: Would convert 1 of 1 posts. + """ + And STDERR should be empty + + When I run `wp post get {POST_ID} --field=post_content` + Then STDOUT should be: + """ +

Title

Text

+ """ + + @broken + Scenario: Skip posts that already contain blocks + Given a WP install + + When I run `wp post create --post_title='Block post' --post_content='

Text

' --porcelain` + Then save STDOUT as {POST_ID} + + When I try `wp post convert-to-blocks {POST_ID}` + Then STDERR should be: + """ + Warning: Post {POST_ID} already contains blocks. + """ + And STDOUT should be: + """ + Success: Converted 0 of 1 posts (1 skipped). + """ + And the return code should be 0 + + @broken + Scenario: Skip posts without content + Given a WP install + + When I run `wp post create --post_title='Empty post' --post_content='' --porcelain` + Then save STDOUT as {POST_ID} + + When I try `wp post convert-to-blocks {POST_ID}` + Then STDERR should be: + """ + Warning: Post {POST_ID} has no content to convert. + """ + And STDOUT should be: + """ + Success: Converted 0 of 1 posts (1 skipped). + """ + And the return code should be 0 + + @broken + Scenario: Report missing posts as failures + Given a WP install + + When I run `wp post create --post_title='Classic post' --post_content='

Text

' --porcelain` + Then save STDOUT as {POST_ID} + + When I try `wp post convert-to-blocks {POST_ID} 99999` + Then STDERR should contain: + """ + Warning: Could not find the post with ID 99999. + """ + And STDERR should contain: + """ + Error: Only converted 1 of 2 posts (1 failed). + """ + And STDOUT should be: + """ + Converted post {POST_ID}. + """ + And the return code should be 1 diff --git a/src/Post_Command.php b/src/Post_Command.php index 41f3e1216..202bdab7d 100644 --- a/src/Post_Command.php +++ b/src/Post_Command.php @@ -1443,6 +1443,134 @@ public function has_block( $args, $assoc_args ) { } } + /** + * Converts the classic (non-block) content of one or more posts to block markup. + * + * Uses the server-side block conversion provided by the Gutenberg plugin + * (see https://github.com/WordPress/gutenberg/pull/82013) to turn classic + * HTML content into serialized block markup. Markup that no block claims + * is kept verbatim inside a Custom HTML block, so nothing is lost. Posts + * that already contain blocks or have no content are skipped. + * + * The conversion does not sanitize the markup. Saving the converted content + * applies the usual kses filtering for the current user context, so a run + * without `--user` is filtered as an untrusted author would be. Run the + * command with `--user=` to keep markup that requires the + * `unfiltered_html` capability, such as iframes or scripts. + * + * Requires a Gutenberg build that provides `gutenberg_html_to_block_markup()`. + * + * ## OPTIONS + * + * ... + * : One or more IDs of posts to convert. + * + * [--dry-run] + * : Preview which posts would be converted, without saving any changes. + * + * ## EXAMPLES + * + * # Convert a single post. + * $ wp post convert-to-blocks 123 + * Converted post 123. + * Success: Converted 1 of 1 posts. + * + * # Convert every post of a post type. + * $ wp post list --post_type=post --format=ids | xargs wp post convert-to-blocks + * Converted post 123. + * Converted post 124. + * Warning: Post 125 already contains blocks. + * Success: Converted 2 of 3 posts (1 skipped). + * + * # Preview a conversion without saving. + * $ wp post convert-to-blocks 123 --dry-run + * Would convert post 123. + * Success: Would convert 1 of 1 posts. + * + * # Run as an administrator to keep markup that requires unfiltered_html. + * $ wp post convert-to-blocks 123 --user=admin + * Converted post 123. + * Success: Converted 1 of 1 posts. + * + * @subcommand convert-to-blocks + */ + public function convert_to_blocks( $args, $assoc_args ) { + if ( ! function_exists( 'gutenberg_html_to_block_markup' ) ) { + WP_CLI::error( 'Server-side block conversion is not available. Activate the Gutenberg plugin from https://github.com/WordPress/gutenberg/pull/82013.' ); + } + + $dry_run = (bool) Utils\get_flag_value( $assoc_args, 'dry-run', false ); + + $total = count( $args ); + $successes = 0; + $errors = 0; + $skips = 0; + + foreach ( $args as $post_id ) { + $post = $this->fetcher->get( $post_id ); + + if ( ! $post ) { + WP_CLI::warning( "Could not find the post with ID {$post_id}." ); + ++$errors; + continue; + } + + if ( '' === trim( (string) $post->post_content ) ) { + WP_CLI::warning( "Post {$post->ID} has no content to convert." ); + ++$skips; + continue; + } + + if ( Block_Processor_Helper::has_blocks( $post->post_content ) ) { + WP_CLI::warning( "Post {$post->ID} already contains blocks." ); + ++$skips; + continue; + } + + $markup = gutenberg_html_to_block_markup( $post->post_content ); + + if ( $dry_run ) { + WP_CLI::log( "Would convert post {$post->ID}." ); + ++$successes; + continue; + } + + $result = wp_update_post( + [ + 'ID' => $post->ID, + 'post_content' => $markup, + ], + true + ); + + if ( is_wp_error( $result ) ) { + WP_CLI::warning( "Failed converting post {$post->ID}: " . $result->get_error_message() ); + ++$errors; + continue; + } + + WP_CLI::log( "Converted post {$post->ID}." ); + ++$successes; + } + + if ( ! $dry_run ) { + Utils\report_batch_operation_results( 'post', 'convert', $total, $successes, $errors, $skips ); + return; + } + + $skipped_message = $skips ? " ({$skips} skipped)" : ''; + + if ( $errors ) { + $failed_skipped_message = " ({$errors} failed" . ( $skips ? ", {$skips} skipped" : '' ) . ')'; + if ( $successes ) { + WP_CLI::error( "Would only convert {$successes} of {$total} posts{$failed_skipped_message}." ); + } + WP_CLI::error( "No posts would be converted{$failed_skipped_message}." ); + } + + WP_CLI::success( "Would convert {$successes} of {$total} posts{$skipped_message}." ); + } + /** * Convert a date-time string with a hyphen separator to a space separator. *