diff -r 3d4e9c994f10 -r a86126ab1dd4 wp/wp-includes/blocks.php --- a/wp/wp-includes/blocks.php Tue Oct 22 16:11:46 2019 +0200 +++ b/wp/wp-includes/blocks.php Tue Dec 15 13:49:49 2020 +0100 @@ -12,12 +12,13 @@ * * @since 5.0.0 * - * @param string|WP_Block_Type $name Block type name including namespace, or alternatively a - * complete WP_Block_Type instance. In case a WP_Block_Type + * @param string|WP_Block_Type $name Block type name including namespace, or alternatively + * a complete WP_Block_Type instance. In case a WP_Block_Type * is provided, the $args parameter will be ignored. * @param array $args { - * Optional. Array of block type arguments. Any arguments may be defined, however the - * ones described below are supported by default. Default empty array. + * Optional. Array of block type arguments. Accepts any public property of `WP_Block_Type`. + * Any arguments may be defined, however the ones described below are supported by default. + * Default empty array. * * @type callable $render_callback Callback used to render blocks of this block type. * } @@ -32,8 +33,8 @@ * * @since 5.0.0 * - * @param string|WP_Block_Type $name Block type name including namespace, or alternatively a - * complete WP_Block_Type instance. + * @param string|WP_Block_Type $name Block type name including namespace, or alternatively + * a complete WP_Block_Type instance. * @return WP_Block_Type|false The unregistered block type on success, or false on failure. */ function unregister_block_type( $name ) { @@ -41,6 +42,217 @@ } /** + * Removes the block asset's path prefix if provided. + * + * @since 5.5.0 + * + * @param string $asset_handle_or_path Asset handle or prefixed path. + * @return string Path without the prefix or the original value. + */ +function remove_block_asset_path_prefix( $asset_handle_or_path ) { + $path_prefix = 'file:'; + if ( 0 !== strpos( $asset_handle_or_path, $path_prefix ) ) { + return $asset_handle_or_path; + } + return substr( + $asset_handle_or_path, + strlen( $path_prefix ) + ); +} + +/** + * Generates the name for an asset based on the name of the block + * and the field name provided. + * + * @since 5.5.0 + * + * @param string $block_name Name of the block. + * @param string $field_name Name of the metadata field. + * @return string Generated asset name for the block's field. + */ +function generate_block_asset_handle( $block_name, $field_name ) { + $field_mappings = array( + 'editorScript' => 'editor-script', + 'script' => 'script', + 'editorStyle' => 'editor-style', + 'style' => 'style', + ); + return str_replace( '/', '-', $block_name ) . + '-' . $field_mappings[ $field_name ]; +} + +/** + * Finds a script handle for the selected block metadata field. It detects + * when a path to file was provided and finds a corresponding asset file + * with details necessary to register the script under automatically + * generated handle name. It returns unprocessed script handle otherwise. + * + * @since 5.5.0 + * + * @param array $metadata Block metadata. + * @param string $field_name Field name to pick from metadata. + * @return string|bool Script handle provided directly or created through + * script's registration, or false on failure. + */ +function register_block_script_handle( $metadata, $field_name ) { + if ( empty( $metadata[ $field_name ] ) ) { + return false; + } + $script_handle = $metadata[ $field_name ]; + $script_path = remove_block_asset_path_prefix( $metadata[ $field_name ] ); + if ( $script_handle === $script_path ) { + return $script_handle; + } + + $script_handle = generate_block_asset_handle( $metadata['name'], $field_name ); + $script_asset_path = realpath( + dirname( $metadata['file'] ) . '/' . + substr_replace( $script_path, '.asset.php', - strlen( '.js' ) ) + ); + if ( ! file_exists( $script_asset_path ) ) { + $message = sprintf( + /* translators: %1: field name. %2: block name */ + __( 'The asset file for the "%1$s" defined in "%2$s" block definition is missing.', 'default' ), + $field_name, + $metadata['name'] + ); + _doing_it_wrong( __FUNCTION__, $message, '5.5.0' ); + return false; + } + $script_asset = require $script_asset_path; + $result = wp_register_script( + $script_handle, + plugins_url( $script_path, $metadata['file'] ), + $script_asset['dependencies'], + $script_asset['version'] + ); + return $result ? $script_handle : false; +} + +/** + * Finds a style handle for the block metadata field. It detects when a path + * to file was provided and registers the style under automatically + * generated handle name. It returns unprocessed style handle otherwise. + * + * @since 5.5.0 + * + * @param array $metadata Block metadata. + * @param string $field_name Field name to pick from metadata. + * @return string|boolean Style handle provided directly or created through + * style's registration, or false on failure. + */ +function register_block_style_handle( $metadata, $field_name ) { + if ( empty( $metadata[ $field_name ] ) ) { + return false; + } + $style_handle = $metadata[ $field_name ]; + $style_path = remove_block_asset_path_prefix( $metadata[ $field_name ] ); + if ( $style_handle === $style_path ) { + return $style_handle; + } + + $style_handle = generate_block_asset_handle( $metadata['name'], $field_name ); + $block_dir = dirname( $metadata['file'] ); + $result = wp_register_style( + $style_handle, + plugins_url( $style_path, $metadata['file'] ), + array(), + filemtime( realpath( "$block_dir/$style_path" ) ) + ); + return $result ? $style_handle : false; +} + +/** + * Registers a block type from metadata stored in the `block.json` file. + * + * @since 5.5.0 + * + * @param string $file_or_folder Path to the JSON file with metadata definition for + * the block or path to the folder where the `block.json` file is located. + * @param array $args { + * Optional. Array of block type arguments. Accepts any public property of `WP_Block_Type`. + * Any arguments may be defined, however the ones described below are supported by default. + * Default empty array. + * + * @type callable $render_callback Callback used to render blocks of this block type. + * } + * @return WP_Block_Type|false The registered block type on success, or false on failure. + */ +function register_block_type_from_metadata( $file_or_folder, $args = array() ) { + $filename = 'block.json'; + $metadata_file = ( substr( $file_or_folder, -strlen( $filename ) ) !== $filename ) ? + trailingslashit( $file_or_folder ) . $filename : + $file_or_folder; + if ( ! file_exists( $metadata_file ) ) { + return false; + } + + $metadata = json_decode( file_get_contents( $metadata_file ), true ); + if ( ! is_array( $metadata ) || empty( $metadata['name'] ) ) { + return false; + } + $metadata['file'] = $metadata_file; + + $settings = array(); + $property_mappings = array( + 'title' => 'title', + 'category' => 'category', + 'parent' => 'parent', + 'icon' => 'icon', + 'description' => 'description', + 'keywords' => 'keywords', + 'attributes' => 'attributes', + 'providesContext' => 'provides_context', + 'usesContext' => 'uses_context', + 'supports' => 'supports', + 'styles' => 'styles', + 'example' => 'example', + ); + + foreach ( $property_mappings as $key => $mapped_key ) { + if ( isset( $metadata[ $key ] ) ) { + $settings[ $mapped_key ] = $metadata[ $key ]; + } + } + + if ( ! empty( $metadata['editorScript'] ) ) { + $settings['editor_script'] = register_block_script_handle( + $metadata, + 'editorScript' + ); + } + + if ( ! empty( $metadata['script'] ) ) { + $settings['script'] = register_block_script_handle( + $metadata, + 'script' + ); + } + + if ( ! empty( $metadata['editorStyle'] ) ) { + $settings['editor_style'] = register_block_style_handle( + $metadata, + 'editorStyle' + ); + } + + if ( ! empty( $metadata['style'] ) ) { + $settings['style'] = register_block_style_handle( + $metadata, + 'style' + ); + } + + return register_block_type( + $metadata['name'], + array_merge( + $settings, + $args + ) + ); +} + +/** * Determine whether a post or content string has blocks. * * This test optimizes for performance rather than strict accuracy, detecting @@ -48,6 +260,7 @@ * you should use the block parser on post content. * * @since 5.0.0 + * * @see parse_blocks() * * @param int|string|WP_Post|null $post Optional. Post content, post ID, or post object. Defaults to global $post. @@ -72,13 +285,14 @@ * you should use the block parser on post content. * * @since 5.0.0 + * * @see parse_blocks() * - * @param string $block_type Full Block type to look for. + * @param string $block_name Full Block type to look for. * @param int|string|WP_Post|null $post Optional. Post content, post ID, or post object. Defaults to global $post. * @return bool Whether the post content contains the specified block. */ -function has_block( $block_type, $post = null ) { +function has_block( $block_name, $post = null ) { if ( ! has_blocks( $post ) ) { return false; } @@ -90,7 +304,30 @@ } } - return false !== strpos( $post, '', $serialized_block_name, $serialized_attributes ); + } + + return sprintf( + '%s', + $serialized_block_name, + $serialized_attributes, + $block_content, + $serialized_block_name + ); +} + +/** + * Returns the content of a block, including comment delimiters, serializing all + * attributes from the given parsed block. + * + * This should be used when preparing a block to be saved to post content. + * Prefer `render_block` when preparing a block for display. Unlike + * `render_block`, this does not evaluate a block's `render_callback`, and will + * instead preserve the markup as parsed. + * + * @since 5.3.1 + * + * @param WP_Block_Parser_Block $block A single parsed block object. + * @return string String of rendered HTML. + */ +function serialize_block( $block ) { + $block_content = ''; + + $index = 0; + foreach ( $block['innerContent'] as $chunk ) { + $block_content .= is_string( $chunk ) ? $chunk : serialize_block( $block['innerBlocks'][ $index++ ] ); + } + + if ( ! is_array( $block['attrs'] ) ) { + $block['attrs'] = array(); + } + + return get_comment_delimited_block_content( + $block['blockName'], + $block['attrs'], + $block_content + ); +} + +/** + * Returns a joined string of the aggregate serialization of the given parsed + * blocks. + * + * @since 5.3.1 + * + * @param WP_Block_Parser_Block[] $blocks Parsed block objects. + * @return string String of rendered HTML. + */ +function serialize_blocks( $blocks ) { + return implode( '', array_map( 'serialize_block', $blocks ) ); +} + +/** + * Filters and sanitizes block content to remove non-allowable HTML from + * parsed block attribute values. + * + * @since 5.3.1 + * + * @param string $text Text that may contain block content. + * @param array[]|string $allowed_html An array of allowed HTML elements + * and attributes, or a context name + * such as 'post'. + * @param string[] $allowed_protocols Array of allowed URL protocols. + * @return string The filtered and sanitized content result. + */ +function filter_block_content( $text, $allowed_html = 'post', $allowed_protocols = array() ) { + $result = ''; + + $blocks = parse_blocks( $text ); + foreach ( $blocks as $block ) { + $block = filter_block_kses( $block, $allowed_html, $allowed_protocols ); + $result .= serialize_block( $block ); + } + + return $result; +} + +/** + * Filters and sanitizes a parsed block to remove non-allowable HTML from block + * attribute values. + * + * @since 5.3.1 + * + * @param WP_Block_Parser_Block $block The parsed block object. + * @param array[]|string $allowed_html An array of allowed HTML + * elements and attributes, or a + * context name such as 'post'. + * @param string[] $allowed_protocols Allowed URL protocols. + * @return array The filtered and sanitized block object result. + */ +function filter_block_kses( $block, $allowed_html, $allowed_protocols = array() ) { + $block['attrs'] = filter_block_kses_value( $block['attrs'], $allowed_html, $allowed_protocols ); + + if ( is_array( $block['innerBlocks'] ) ) { + foreach ( $block['innerBlocks'] as $i => $inner_block ) { + $block['innerBlocks'][ $i ] = filter_block_kses( $inner_block, $allowed_html, $allowed_protocols ); + } + } + + return $block; +} + +/** + * Filters and sanitizes a parsed block attribute value to remove non-allowable + * HTML. + * + * @since 5.3.1 + * + * @param string[]|string $value The attribute value to filter. + * @param array[]|string $allowed_html An array of allowed HTML elements + * and attributes, or a context name + * such as 'post'. + * @param string[] $allowed_protocols Array of allowed URL protocols. + * @return string[]|string The filtered and sanitized result. + */ +function filter_block_kses_value( $value, $allowed_html, $allowed_protocols = array() ) { + if ( is_array( $value ) ) { + foreach ( $value as $key => $inner_value ) { + $filtered_key = filter_block_kses_value( $key, $allowed_html, $allowed_protocols ); + $filtered_value = filter_block_kses_value( $inner_value, $allowed_html, $allowed_protocols ); + + if ( $filtered_key !== $key ) { + unset( $value[ $key ] ); + } + + $value[ $filtered_key ] = $filtered_value; + } + } elseif ( is_string( $value ) ) { + return wp_kses( $value, $allowed_html, $allowed_protocols ); + } + + return $value; +} + +/** * Parses blocks out of a content string, and renders those appropriate for the excerpt. * * As the excerpt should be a small string of text relevant to the full post content, @@ -212,67 +650,74 @@ * * @since 5.0.0 * - * @global WP_Post $post The post to edit. + * @global WP_Post $post The post to edit. + * @global WP_Query $wp_query WordPress Query object. * - * @param array $block A single parsed block object. + * @param array $parsed_block A single parsed block object. * @return string String of rendered HTML. */ -function render_block( $block ) { - global $post; +function render_block( $parsed_block ) { + global $post, $wp_query; /** - * Allows render_block() to be shortcircuited, by returning a non-null value. + * Allows render_block() to be short-circuited, by returning a non-null value. * * @since 5.1.0 * - * @param string $pre_render The pre-rendered content. Default null. - * @param array $block The block being rendered. + * @param string|null $pre_render The pre-rendered content. Default null. + * @param array $parsed_block The block being rendered. */ - $pre_render = apply_filters( 'pre_render_block', null, $block ); + $pre_render = apply_filters( 'pre_render_block', null, $parsed_block ); if ( ! is_null( $pre_render ) ) { return $pre_render; } - $source_block = $block; + $source_block = $parsed_block; /** * Filters the block being rendered in render_block(), before it's processed. * * @since 5.1.0 * - * @param array $block The block being rendered. - * @param array $source_block An un-modified copy of $block, as it appeared in the source content. + * @param array $parsed_block The block being rendered. + * @param array $source_block An un-modified copy of $parsed_block, as it appeared in the source content. */ - $block = apply_filters( 'render_block_data', $block, $source_block ); + $parsed_block = apply_filters( 'render_block_data', $parsed_block, $source_block ); + + $context = array(); - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); - $is_dynamic = $block['blockName'] && null !== $block_type && $block_type->is_dynamic(); - $block_content = ''; - $index = 0; + if ( $post instanceof WP_Post ) { + $context['postId'] = $post->ID; - foreach ( $block['innerContent'] as $chunk ) { - $block_content .= is_string( $chunk ) ? $chunk : render_block( $block['innerBlocks'][ $index++ ] ); + /* + * The `postType` context is largely unnecessary server-side, since the ID + * is usually sufficient on its own. That being said, since a block's + * manifest is expected to be shared between the server and the client, + * it should be included to consistently fulfill the expectation. + */ + $context['postType'] = $post->post_type; } - if ( ! is_array( $block['attrs'] ) ) { - $block['attrs'] = array(); - } - - if ( $is_dynamic ) { - $global_post = $post; - $block_content = $block_type->render( $block['attrs'], $block_content ); - $post = $global_post; + if ( $wp_query instanceof WP_Query && isset( $wp_query->tax_query->queried_terms['category'] ) ) { + $context['query'] = array( 'categoryIds' => array() ); + foreach ( $wp_query->tax_query->queried_terms['category']['terms'] as $category_slug_or_id ) { + $context['query']['categoryIds'][] = 'slug' === $wp_query->tax_query->queried_terms['category']['field'] ? get_cat_ID( $category_slug_or_id ) : $category_slug_or_id; + } } /** - * Filters the content of a single block. + * Filters the default context provided to a rendered block. * - * @since 5.0.0 + * @since 5.5.0 * - * @param string $block_content The block content about to be appended. - * @param array $block The full block, including name and attributes. + * @param array $context Default context. + * @param array $parsed_block Block being rendered, filtered by `render_block_data`. */ - return apply_filters( 'render_block', $block_content, $block ); + $context = apply_filters( 'render_block_context', $context, $parsed_block ); + + $block = new WP_Block( $parsed_block, $context ); + + return $block->render(); } /** @@ -281,7 +726,7 @@ * @since 5.0.0 * * @param string $content Post content. - * @return array Array of parsed block objects. + * @return array[] Array of parsed block objects. */ function parse_blocks( $content ) { /** @@ -301,9 +746,8 @@ * Parses dynamic blocks out of `post_content` and re-renders them. * * @since 5.0.0 - * @global WP_Post $post The post to edit. * - * @param string $content Post content. + * @param string $content Post content. * @return string Updated post content. */ function do_blocks( $content ) { @@ -357,3 +801,31 @@ function block_version( $content ) { return has_blocks( $content ) ? 1 : 0; } + +/** + * Registers a new block style. + * + * @since 5.3.0 + * + * @param string $block_name Block type name including namespace. + * @param array $style_properties Array containing the properties of the style name, + * label, style (name of the stylesheet to be enqueued), + * inline_style (string containing the CSS to be added). + * @return boolean True if the block style was registered with success and false otherwise. + */ +function register_block_style( $block_name, $style_properties ) { + return WP_Block_Styles_Registry::get_instance()->register( $block_name, $style_properties ); +} + +/** + * Unregisters a block style. + * + * @since 5.3.0 + * + * @param string $block_name Block type name including namespace. + * @param array $block_style_name Block style name. + * @return boolean True if the block style was unregistered with success and false otherwise. + */ +function unregister_block_style( $block_name, $block_style_name ) { + return WP_Block_Styles_Registry::get_instance()->unregister( $block_name, $block_style_name ); +}