diff --git a/.wp-env.json b/.wp-env.json index 10f54621c..36d90c75a 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -15,6 +15,7 @@ "wp-content": "./wp-content", "wp-content/mu-plugins/0-sandbox.php": "./.wp-env/0-sandbox.php", "wp-content/uploads/wporg_events.sql": "./.wp-env/wporg_events.sql", - "wp-content/uploads/wporg_locales.sql": "./.wp-env/wporg_locales.sql" + "wp-content/uploads/wporg_locales.sql": "./.wp-env/wporg_locales.sql", + "bin": "./bin" } } diff --git a/bin/import-test-content.php b/bin/import-test-content.php new file mode 100644 index 000000000..62fb2f871 --- /dev/null +++ b/bin/import-test-content.php @@ -0,0 +1,91 @@ +#!/usr/bin/php + $v ) { + if ( is_array( $v ) ) { + $meta[ $k ] = implode( ',', $v ); + } + } + + return $meta; +} + +/** + * Import posts from a remote REST API to the local test site. + * + * @param string $rest_url The remote REST API endpoint URL. + */ +function import_rest_to_posts( $rest_url ) { + $response = wp_remote_get( $rest_url ); + $status_code = wp_remote_retrieve_response_code( $response ); + + if ( is_wp_error( $response ) ) { + die( esc_html( $response->get_error_message() ) ); + } elseif ( 200 !== wp_remote_retrieve_response_code( $response ) ) { + die( esc_html( "HTTP Error $status_code \n" ) ); + } + + $body = wp_remote_retrieve_body( $response ); + $data = json_decode( $body ); + + foreach ( $data as $post ) { + echo esc_html( "Got {$post->type} {$post->id} {$post->slug}\n" ); + + // Surely there's a neater way to do this. + $newpost = array( + 'import_id' => $post->id, + 'post_date' => gmdate( 'Y-m-d H:i:s', strtotime( $post->date ) ), + 'post_name' => $post->slug, + 'post_title' => $post->title, + 'post_status' => $post->status, + 'post_type' => $post->type, + 'post_title' => $post->title->rendered, + 'post_content' => ( $post->content_raw ?? $post->content->rendered ), + 'post_parent' => $post->parent, + 'comment_status' => $post->comment_status, + 'meta_input' => sanitize_meta_input( $post->meta ), + ); + + $new_post_id = wp_insert_post( $newpost, true ); + + if ( is_wp_error( $new_post_id ) ) { + die( esc_html( $new_post_id->get_error_message() ) ); + } + + echo esc_html( "Inserted $post->type $post->id as $new_post_id\n" ); + } +} + +import_rest_to_posts( 'https://learn.wordpress.org/wp-json/wp/v2/wporg_workshop?context=export&per_page=50' ); +import_rest_to_posts( 'https://learn.wordpress.org/wp-json/wp/v2/lesson-plan?context=export&per_page=50' ); diff --git a/bin/index.sh b/bin/index.sh index 1d41870ea..aa3ac2cda 100755 --- a/bin/index.sh +++ b/bin/index.sh @@ -28,3 +28,6 @@ npm run wp-env run cli wp rewrite structure '/%postname%/' # Import tables npm run wp-env run cli wp db import wp-content/uploads/wporg_events.sql npm run wp-env run cli wp db import wp-content/uploads/wporg_locales.sql + +# Import content +npm run wp-env run cli "php bin/import-test-content.php" diff --git a/wp-content/plugins/wporg-learn/inc/export.php b/wp-content/plugins/wporg-learn/inc/export.php new file mode 100644 index 000000000..ff2b2e2db --- /dev/null +++ b/wp-content/plugins/wporg-learn/inc/export.php @@ -0,0 +1,150 @@ + __NAMESPACE__ . '\show_post_content_raw', + 'schema' => array( + 'type' => 'string', + 'context' => array( 'wporg_export' ), + ), + ) + ); + + add_filter( "rest_{$post_type}_item_schema", __NAMESPACE__ . '\add_export_context_to_schema' ); +} + +/** + * Filter a CPT item schema and make it so that every item with 'view' context also has 'export' context. + * + * @param array $schema The schema object. + */ +function add_export_context_to_schema( $schema ) { + update_schema_array_recursive( $schema ); + + return $schema; +} + +/** + * Find every item in the schema that has a 'view' context, and add an 'export' context to it. + * Had to use a recursive function because array_walk_recursive only walks leaf nodes. + * + * @param array $schema The schema object. + */ +function update_schema_array_recursive( &$schema ) { + foreach ( $schema as $key => &$value ) { + // Head recursion + if ( is_array( $value ) ) { + update_schema_array_recursive( $value ); + } + if ( 'context' === $key && in_array( 'view', $value ) ) { + $value[] = 'wporg_export'; + } + } +} + +/** + * Given an array of blocks, return an array of just the names of those blocks. + * + * @param array $blocks An array of blocks. + * @return array An array of block names. + */ +function get_all_block_names( $blocks ) { + $block_names = array(); + if ( ! $blocks ) { + return array(); + } + foreach ( $blocks as $block ) { + if ( null !== $block['blockName'] ) { + $block_names[] = $block['blockName']; + if ( $block['innerBlocks'] ) { + // Recursive call to get inner blocks + $block_names = array_merge( $block_names, get_all_block_names( $block['innerBlocks'] ) ); + } + } + } + + return array_unique( $block_names ); +} + +/** + * Callback: If a post contains only allowed blocks, then return the raw block markup for the post. + * + * @param array $object The post object relating to the REST request. + * @param string $field_name The field name. + * @param array $request The request object. + * + * @return string The raw post content, if it contains only allowed blocks; a placeholder string otherwise. + */ +function show_post_content_raw( $object, $field_name, $request ) { + + /** + * Filter: Modify the list of blocks permitted in posts available via the 'export' context. + * Posts containing any other blocks will not be exported. + * + * @param array $allowed_blocks An array of allowed block names. Simple wildcards are permitted, like 'core/*'. + */ + $allowed_blocks = apply_filters( 'allow_raw_block_export', array( + 'core/*', + 'wporg/*', + // other allowed blocks: + 'jetpack/image-compare', + 'jetpack/tiled-gallery', + 'syntaxhighlighter/code', + ) ); + + if ( ! empty( $object['id'] ) ) { + $post = get_post( $object['id'] ); + } else { + $post = get_post(); + } + + // Exit early if the post contains any blocks that are not explicitly allowed. + if ( $post && has_blocks( $post->post_content ) || true ) { + + $regexes = array(); + foreach ( $allowed_blocks as $allowed_block_name ) { + $regexes[] = strtr( preg_quote( $allowed_block_name, '#' ), array( '\*' => '.*' ) ); + } + + $regex = '#^(' . implode( '|', $regexes ) . ')$#'; + + $blocks = parse_blocks( $post->post_content ); + $block_names = get_all_block_names( $blocks ); + + foreach ( $block_names as $block_name ) { + // If it contains a disallowed block, then return no content. + // Better to raise an error instead? + if ( ! preg_match( $regex, $block_name ) ) { + return '

Post contains a disallowed block ' . esc_html( $block_name ) . '

'; + } + } + } + + return $post->post_content; +} diff --git a/wp-content/plugins/wporg-learn/wporg-learn.php b/wp-content/plugins/wporg-learn/wporg-learn.php index 6f88782a6..40bc3c3d7 100644 --- a/wp-content/plugins/wporg-learn/wporg-learn.php +++ b/wp-content/plugins/wporg-learn/wporg-learn.php @@ -77,6 +77,7 @@ function load_files() { require_once get_includes_path() . 'profiles.php'; require_once get_includes_path() . 'sensei.php'; require_once get_includes_path() . 'taxonomy.php'; + require_once get_includes_path() . 'export.php'; } /**