diff --git a/5.7-changes-2.diff b/5.7-changes-2.diff new file mode 100644 index 00000000000000..cfc0c95e0d5584 --- /dev/null +++ b/5.7-changes-2.diff @@ -0,0 +1,2157 @@ +diff --git a/lib/blocks.php b/lib/blocks.php +index f4f5a6536c..d1428770e3 100644 +--- a/lib/blocks.php ++++ b/lib/blocks.php +@@ -49,42 +49,45 @@ function gutenberg_reregister_core_block_types() { + ), + 'block_names' => array_merge( + array( +- 'archives.php' => 'core/archives', +- 'block.php' => 'core/block', +- 'calendar.php' => 'core/calendar', +- 'categories.php' => 'core/categories', +- 'cover.php' => 'core/cover', +- 'latest-comments.php' => 'core/latest-comments', +- 'latest-posts.php' => 'core/latest-posts', +- 'navigation.php' => 'core/navigation', +- 'navigation-link.php' => 'core/navigation-link', +- 'rss.php' => 'core/rss', +- 'search.php' => 'core/search', +- 'shortcode.php' => 'core/shortcode', +- 'social-link.php' => 'core/social-link', +- 'tag-cloud.php' => 'core/tag-cloud', +- 'post-author.php' => 'core/post-author', +- 'post-comment.php' => 'core/post-comment', +- 'post-comment-author.php' => 'core/post-comment-author', +- 'post-comment-content.php' => 'core/post-comment-content', +- 'post-comment-date.php' => 'core/post-comment-date', +- 'post-comments.php' => 'core/post-comments', +- 'post-comments-count.php' => 'core/post-comments-count', +- 'post-comments-form.php' => 'core/post-comments-form', +- 'post-content.php' => 'core/post-content', +- 'post-date.php' => 'core/post-date', +- 'post-excerpt.php' => 'core/post-excerpt', +- 'post-featured-image.php' => 'core/post-featured-image', +- 'post-hierarchical-terms.php' => 'core/post-hierarchical-terms', +- 'post-tags.php' => 'core/post-tags', +- 'post-title.php' => 'core/post-title', +- 'query.php' => 'core/query', +- 'query-loop.php' => 'core/query-loop', +- 'query-pagination.php' => 'core/query-pagination', +- 'site-logo.php' => 'core/site-logo', +- 'site-tagline.php' => 'core/site-tagline', +- 'site-title.php' => 'core/site-title', +- 'template-part.php' => 'core/template-part', ++ 'archives.php' => 'core/archives', ++ 'block.php' => 'core/block', ++ 'calendar.php' => 'core/calendar', ++ 'categories.php' => 'core/categories', ++ 'cover.php' => 'core/cover', ++ 'latest-comments.php' => 'core/latest-comments', ++ 'latest-posts.php' => 'core/latest-posts', ++ 'navigation.php' => 'core/navigation', ++ 'navigation-link.php' => 'core/navigation-link', ++ 'rss.php' => 'core/rss', ++ 'search.php' => 'core/search', ++ 'shortcode.php' => 'core/shortcode', ++ 'social-link.php' => 'core/social-link', ++ 'tag-cloud.php' => 'core/tag-cloud', ++ 'post-author.php' => 'core/post-author', ++ 'post-comment.php' => 'core/post-comment', ++ 'post-comment-author.php' => 'core/post-comment-author', ++ 'post-comment-content.php' => 'core/post-comment-content', ++ 'post-comment-date.php' => 'core/post-comment-date', ++ 'post-comments.php' => 'core/post-comments', ++ 'post-comments-count.php' => 'core/post-comments-count', ++ 'post-comments-form.php' => 'core/post-comments-form', ++ 'post-content.php' => 'core/post-content', ++ 'post-date.php' => 'core/post-date', ++ 'post-excerpt.php' => 'core/post-excerpt', ++ 'post-featured-image.php' => 'core/post-featured-image', ++ 'post-hierarchical-terms.php' => 'core/post-hierarchical-terms', ++ 'post-tags.php' => 'core/post-tags', ++ 'post-title.php' => 'core/post-title', ++ 'query.php' => 'core/query', ++ 'query-loop.php' => 'core/query-loop', ++ 'query-pagination.php' => 'core/query-pagination', ++ 'query-pagination-next.php' => 'core/query-pagination-next', ++ 'query-pagination-numbers.php' => 'core/query-pagination-numbers', ++ 'query-pagination-previous.php' => 'core/query-pagination-previous', ++ 'site-logo.php' => 'core/site-logo', ++ 'site-tagline.php' => 'core/site-tagline', ++ 'site-title.php' => 'core/site-title', ++ 'template-part.php' => 'core/template-part', + ) + ), + ), +@@ -164,29 +167,27 @@ function gutenberg_register_core_block_styles( $block_name ) { + + $block_name = str_replace( 'core/', '', $block_name ); + +- $style_path = is_rtl() +- ? "build/block-library/blocks/$block_name/style-rtl.css" +- : "build/block-library/blocks/$block_name/style.css"; +- $editor_style_path = is_rtl() +- ? "build/block-library/blocks/$block_name/style-editor-rtl.css" +- : "build/block-library/blocks/$block_name/style-editor.css"; ++ $style_path = "build/block-library/blocks/$block_name/style.css"; ++ $editor_style_path = "build/block-library/blocks/$block_name/style-editor.css"; + + if ( file_exists( gutenberg_dir_path() . $style_path ) ) { + wp_register_style( +- 'wp-block-' . $block_name, ++ "wp-block-{$block_name}", + gutenberg_url( $style_path ), + array(), + filemtime( gutenberg_dir_path() . $style_path ) + ); ++ wp_style_add_data( "wp-block-{$block_name}", 'rtl', 'replace' ); + } + + if ( file_exists( gutenberg_dir_path() . $editor_style_path ) ) { + wp_register_style( +- 'wp-block-' . $block_name . '-editor', ++ "wp-block-{$block_name}-editor", + gutenberg_url( $editor_style_path ), + array(), + filemtime( gutenberg_dir_path() . $editor_style_path ) + ); ++ wp_style_add_data( "wp-block-{$block_name}-editor", 'rtl', 'replace' ); + } + } + +diff --git a/lib/class-wp-rest-pattern-directory-controller.php b/lib/class-wp-rest-pattern-directory-controller.php +new file mode 100644 +index 0000000000..2d86056335 +--- /dev/null ++++ b/lib/class-wp-rest-pattern-directory-controller.php +@@ -0,0 +1,288 @@ ++namespace = '__experimental'; ++ $this->rest_base = 'pattern-directory'; ++ } ++ ++ /** ++ * Registers the necessary REST API routes. ++ */ ++ public function register_routes() { ++ register_rest_route( ++ $this->namespace, ++ '/' . $this->rest_base . '/patterns', ++ array( ++ array( ++ 'methods' => WP_REST_Server::READABLE, ++ 'callback' => array( $this, 'get_items' ), ++ 'permission_callback' => array( $this, 'get_items_permissions_check' ), ++ 'args' => $this->get_collection_params(), ++ ), ++ 'schema' => array( $this, 'get_public_item_schema' ), ++ ) ++ ); ++ } ++ ++ /** ++ * Checks whether a given request has permission to view the local pattern directory. ++ * ++ * @since 5.8.0 ++ * ++ * @param WP_REST_Request $request Full details about the request. ++ * ++ * @return WP_Error|bool True if the request has permission, WP_Error object otherwise. ++ */ ++ public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Method must match signature of parent class. ++ if ( current_user_can( 'edit_posts' ) ) { ++ return true; ++ } ++ ++ foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) { ++ if ( current_user_can( $post_type->cap->edit_posts ) ) { ++ return true; ++ } ++ } ++ ++ return new WP_Error( ++ 'rest_pattern_directory_cannot_view', ++ __( 'Sorry, you are not allowed to browse the local block pattern directory.', 'gutenberg' ), ++ array( 'status' => rest_authorization_required_code() ) ++ ); ++ } ++ ++ /** ++ * Search and retrieve block patterns metadata ++ * ++ * @since 5.8.0 ++ * ++ * @param WP_REST_Request $request Full details about the request. ++ * ++ * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure. ++ */ ++ public function get_items( $request ) { ++ $query_args = array(); ++ $category_ids = $request['category']; ++ $search_term = $request['search']; ++ ++ if ( $category_ids ) { ++ $query_args['pattern-categories'] = $category_ids; ++ } ++ ++ if ( $search_term ) { ++ $query_args['search'] = $search_term; ++ } ++ ++ $api_url = add_query_arg( ++ array_map( 'rawurlencode', $query_args ), ++ 'http://api.wordpress.org/patterns/1.0/' ++ ); ++ ++ if ( wp_http_supports( array( 'ssl' ) ) ) { ++ $api_url = set_url_scheme( $api_url, 'https' ); ++ } ++ ++ $wporg_response = wp_remote_get( $api_url ); ++ $raw_patterns = json_decode( wp_remote_retrieve_body( $wporg_response ) ); ++ ++ if ( is_wp_error( $wporg_response ) ) { ++ $wporg_response->add_data( array( 'status' => 500 ) ); ++ ++ return $wporg_response; ++ } ++ ++ // Make sure w.org returned valid data. ++ if ( ! is_array( $raw_patterns ) ) { ++ return new WP_Error( ++ 'pattern_api_failed', ++ sprintf( ++ /* translators: %s: Support forums URL. */ ++ __( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server’s configuration. If you continue to have problems, please try the support forums.', 'gutenberg' ), ++ __( 'https://wordpress.org/support/forums/', 'gutenberg' ) ++ ), ++ array( ++ 'status' => 500, ++ 'response' => wp_remote_retrieve_body( $wporg_response ), ++ ) ++ ); ++ } ++ ++ $response = array(); ++ ++ if ( $raw_patterns ) { ++ foreach ( $raw_patterns as $pattern ) { ++ $response[] = $this->prepare_response_for_collection( ++ $this->prepare_item_for_response( $pattern, $request ) ++ ); ++ } ++ } ++ ++ return new WP_REST_Response( $response ); ++ } ++ ++ /** ++ * Prepare a raw pattern before it's output in an API response. ++ * ++ * @since 5.8.0 ++ * ++ * @param object $raw_pattern A pattern from api.wordpress.org, before any changes. ++ * @param WP_REST_Request $request Request object. ++ * ++ * @return WP_REST_Response ++ */ ++ public function prepare_item_for_response( $raw_pattern, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Method must match signature of parent class. ++ $prepared_pattern = array( ++ 'id' => absint( $raw_pattern->id ), ++ 'title' => sanitize_text_field( $raw_pattern->title->rendered ), ++ 'content' => wp_kses_post( $raw_pattern->content->rendered ), ++ 'categories' => array_map( 'sanitize_title', $raw_pattern->category_slugs ), ++ 'keywords' => array_map( 'sanitize_title', $raw_pattern->keyword_slugs ), ++ 'description' => sanitize_text_field( $raw_pattern->meta->wpop_description ), ++ 'viewport_width' => absint( $raw_pattern->meta->wpop_viewport_width ), ++ ++ ); ++ ++ $prepared_pattern = $this->add_additional_fields_to_object( $prepared_pattern, $request ); ++ ++ $response = new WP_REST_Response( $prepared_pattern ); ++ ++ /** ++ * Filters the REST API response for a pattern. ++ * ++ * @since 5.8.0 ++ * ++ * @param WP_REST_Response $response The response object. ++ * @param object $raw_pattern The unprepared pattern. ++ * @param WP_REST_Request $request The request object. ++ */ ++ return apply_filters( 'rest_prepare_application_password', $response, $raw_pattern, $request ); ++ } ++ ++ /** ++ * Retrieves the pattern's schema, conforming to JSON Schema. ++ * ++ * @since 5.8.0 ++ * ++ * @return array Item schema data. ++ */ ++ public function get_item_schema() { ++ if ( $this->schema ) { ++ return $this->add_additional_fields_schema( $this->schema ); ++ } ++ ++ $this->schema = array( ++ '$schema' => 'http://json-schema.org/draft-04/schema#', ++ 'title' => 'pattern-directory-item', ++ 'type' => 'object', ++ 'properties' => array( ++ 'id' => array( ++ 'description' => __( 'The pattern ID.', 'gutenberg' ), ++ 'type' => 'integer', ++ 'minimum' => 1, ++ 'context' => array( 'view', 'embed' ), ++ ), ++ ++ 'title' => array( ++ 'description' => __( 'The pattern title, in human readable format.', 'gutenberg' ), ++ 'type' => 'string', ++ 'minLength' => 1, ++ 'context' => array( 'view', 'embed' ), ++ ), ++ ++ 'content' => array( ++ 'description' => __( 'The pattern content.', 'gutenberg' ), ++ 'type' => 'string', ++ 'minLength' => 1, ++ 'context' => array( 'view', 'embed' ), ++ ), ++ ++ 'categories' => array( ++ 'description' => __( "The pattern's category slugs.", 'gutenberg' ), ++ 'type' => 'array', ++ 'uniqueItems' => true, ++ 'items' => array( 'type' => 'string' ), ++ 'context' => array( 'view', 'embed' ), ++ ), ++ ++ 'keywords' => array( ++ 'description' => __( "The pattern's keyword slugs.", 'gutenberg' ), ++ 'type' => 'array', ++ 'uniqueItems' => true, ++ 'items' => array( 'type' => 'string' ), ++ 'context' => array( 'view', 'embed' ), ++ ), ++ ++ 'description' => array( ++ 'description' => __( 'A description of the pattern.', 'gutenberg' ), ++ 'type' => 'string', ++ 'minLength' => 1, ++ 'context' => array( 'view', 'embed' ), ++ ), ++ ++ 'viewport_width' => array( ++ 'description' => __( 'The preferred width of the viewport when previewing a pattern, in pixels.', 'gutenberg' ), ++ 'type' => 'integer', ++ 'context' => array( 'view', 'embed' ), ++ ), ++ ), ++ ); ++ ++ return $this->add_additional_fields_schema( $this->schema ); ++ } ++ ++ /** ++ * Retrieves the search params for the patterns collection. ++ * ++ * @since 5.5.0 ++ * ++ * @return array Collection parameters. ++ */ ++ public function get_collection_params() { ++ $query_params = parent::get_collection_params(); ++ ++ // Pagination is not supported. ++ unset( $query_params['page'] ); ++ unset( $query_params['per_page'] ); ++ ++ $query_params['search']['minLength'] = 1; ++ $query_params['context']['default'] = 'view'; ++ ++ $query_params['category'] = array( ++ 'description' => __( 'Limit results to those matching a category ID.', 'gutenberg' ), ++ 'type' => 'integer', ++ 'minimum' => 1, ++ ); ++ ++ /** ++ * Filter collection parameters for the pattern directory controller. ++ * ++ * @since 5.5.0 ++ * ++ * @param array $query_params JSON Schema-formatted collection parameters. ++ */ ++ return apply_filters( 'rest_pattern_directory_collection_params', $query_params ); ++ } ++} +diff --git a/lib/class-wp-theme-json-resolver.php b/lib/class-wp-theme-json-resolver.php +index 67f5f7e12c..d54ae84c8d 100644 +--- a/lib/class-wp-theme-json-resolver.php ++++ b/lib/class-wp-theme-json-resolver.php +@@ -76,6 +76,32 @@ class WP_Theme_JSON_Resolver { + * containing the a translatable path from theme.json and an array + * of properties that are translatable. + * ++ * For example, given this input: ++ * ++ * { ++ * "settings": { ++ * "*": { ++ * "typography": { ++ * "fontSizes": [ "name" ], ++ * "fontStyles": [ "name" ] ++ * } ++ * } ++ * } ++ * } ++ * ++ * will return this output: ++ * ++ * [ ++ * 0 => [ ++ * 'path' => [ 'settings', '*', 'typography', 'fontSizes' ], ++ * 'translatable_keys' => [ 'name' ] ++ * ], ++ * 1 => [ ++ * 'path' => [ 'settings', '*', 'typography', 'fontStyles' ], ++ * 'translatable_keys' => [ 'name'] ++ * ] ++ * ] ++ * + * @param array $file_structure_partial A part of a theme.json i18n tree. + * @param array $current_path An array with a path on the theme.json i18n tree. + * +@@ -110,7 +136,6 @@ class WP_Theme_JSON_Resolver { + if ( null === $theme_json_i18n ) { + $file_structure = self::get_from_file( __DIR__ . '/experimental-i18n-theme.json' ); + $theme_json_i18n = self::theme_json_i18_file_structure_to_preset_paths( $file_structure ); +- + } + return $theme_json_i18n; + } +@@ -123,29 +148,37 @@ class WP_Theme_JSON_Resolver { + * Default 'default'. + */ + private static function translate_presets( &$theme_json_structure, $domain = 'default' ) { ++ if ( ! isset( $theme_json_structure['settings'] ) ) { ++ return; ++ } ++ + $preset_to_translate = self::get_presets_to_translate(); +- foreach ( $theme_json_structure as &$context_value ) { +- if ( empty( $context_value ) ) { ++ foreach ( $theme_json_structure['settings'] as &$settings ) { ++ if ( empty( $settings ) ) { + continue; + } ++ + foreach ( $preset_to_translate as $preset ) { +- $path = $preset['path']; ++ $path = array_slice( $preset['path'], 2 ); + $translatable_keys = $preset['translatable_keys']; +- $array_to_translate = gutenberg_experimental_get( $context_value, $path, null ); ++ $array_to_translate = gutenberg_experimental_get( $settings, $path, null ); + if ( null === $array_to_translate ) { + continue; + } ++ + foreach ( $array_to_translate as &$item_to_translate ) { + foreach ( $translatable_keys as $translatable_key ) { + if ( empty( $item_to_translate[ $translatable_key ] ) ) { + continue; + } ++ + // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain + $item_to_translate[ $translatable_key ] = translate( $item_to_translate[ $translatable_key ], $domain ); + // phpcs:enable + } + } +- gutenberg_experimental_set( $context_value, $path, $array_to_translate ); ++ ++ gutenberg_experimental_set( $settings, $path, $array_to_translate ); + } + } + } +@@ -160,7 +193,8 @@ class WP_Theme_JSON_Resolver { + return self::$core; + } + +- $config = self::get_from_file( __DIR__ . '/experimental-default-theme.json' ); ++ $all_blocks = WP_Theme_JSON::ALL_BLOCKS_NAME; ++ $config = self::get_from_file( __DIR__ . '/experimental-default-theme.json' ); + self::translate_presets( $config ); + + // Start i18n logic to remove when JSON i18 strings are extracted. +@@ -178,8 +212,8 @@ class WP_Theme_JSON_Resolver { + 'vivid-cyan-blue' => __( 'Vivid cyan blue', 'gutenberg' ), + 'vivid-purple' => __( 'Vivid purple', 'gutenberg' ), + ); +- if ( ! empty( $config['global']['settings']['color']['palette'] ) ) { +- foreach ( $config['global']['settings']['color']['palette'] as &$color ) { ++ if ( ! empty( $config['settings'][ $all_blocks ]['color']['palette'] ) ) { ++ foreach ( $config['settings'][ $all_blocks ]['color']['palette'] as &$color ) { + $color['name'] = $default_colors_i18n[ $color['slug'] ]; + } + } +@@ -198,8 +232,8 @@ class WP_Theme_JSON_Resolver { + 'electric-grass' => __( 'Electric grass', 'gutenberg' ), + 'midnight' => __( 'Midnight', 'gutenberg' ), + ); +- if ( ! empty( $config['global']['settings']['color']['gradients'] ) ) { +- foreach ( $config['global']['settings']['color']['gradients'] as &$gradient ) { ++ if ( ! empty( $config['settings'][ $all_blocks ]['color']['gradients'] ) ) { ++ foreach ( $config['settings'][ $all_blocks ]['color']['gradients'] as &$gradient ) { + $gradient['name'] = $default_gradients_i18n[ $gradient['slug'] ]; + } + } +@@ -211,8 +245,8 @@ class WP_Theme_JSON_Resolver { + 'large' => __( 'Large', 'gutenberg' ), + 'huge' => __( 'Huge', 'gutenberg' ), + ); +- if ( ! empty( $config['global']['settings']['typography']['fontSizes'] ) ) { +- foreach ( $config['global']['settings']['typography']['fontSizes'] as &$font_size ) { ++ if ( ! empty( $config['settings'][ $all_blocks ]['typography']['fontSizes'] ) ) { ++ foreach ( $config['settings'][ $all_blocks ]['typography']['fontSizes'] as &$font_size ) { + $font_size['name'] = $default_font_sizes_i18n[ $font_size['slug'] ]; + } + } +@@ -325,7 +359,7 @@ class WP_Theme_JSON_Resolver { + $config = $decoded_data; + } + } +- self::$user = new WP_Theme_JSON( $config, true ); ++ self::$user = new WP_Theme_JSON( $config ); + + return self::$user; + } +diff --git a/lib/class-wp-theme-json.php b/lib/class-wp-theme-json.php +index c8cd12f410..7a457e5d39 100644 +--- a/lib/class-wp-theme-json.php ++++ b/lib/class-wp-theme-json.php +@@ -16,7 +16,7 @@ class WP_Theme_JSON { + * + * @var array + */ +- private $contexts = null; ++ private $theme_json = null; + + /** + * Holds block metadata extracted from block.json +@@ -28,29 +28,43 @@ class WP_Theme_JSON { + private static $blocks_metadata = null; + + /** +- * The name of the global context. ++ * How to address all the blocks ++ * in the theme.json file. ++ */ ++ const ALL_BLOCKS_NAME = 'defaults'; ++ ++ /** ++ * The CSS selector for the * block, ++ * only using to generate presets. ++ * ++ * @var string ++ */ ++ const ALL_BLOCKS_SELECTOR = ':root'; ++ ++ /** ++ * How to address the root block ++ * in the theme.json file. + * + * @var string + */ +- const GLOBAL_NAME = 'global'; ++ const ROOT_BLOCK_NAME = 'root'; + + /** +- * The CSS selector for the global context. ++ * The CSS selector for the root block. + * + * @var string + */ +- const GLOBAL_SELECTOR = ':root'; ++ const ROOT_BLOCK_SELECTOR = ':root'; + + /** +- * The supported properties of the global context. ++ * The supported properties of the root block. + * + * @var array + */ +- const GLOBAL_SUPPORTS = array( ++ const ROOT_BLOCK_SUPPORTS = array( + '--wp--style--color--link', + 'background', + 'backgroundColor', +- 'border', + 'color', + 'fontFamily', + 'fontSize', +@@ -62,12 +76,12 @@ class WP_Theme_JSON { + ); + + /** +- * Data schema of each context within a theme.json. ++ * Data schema of each block within a theme.json. + * + * Example: + * + * { +- * 'context-one': { ++ * 'block-one': { + * 'styles': { + * 'color': { + * 'background': 'color' +@@ -79,7 +93,7 @@ class WP_Theme_JSON { + * } + * } + * }, +- * 'context-two': { ++ * 'block-two': { + * 'styles': { + * 'color': { + * 'link': 'color' +@@ -92,6 +106,9 @@ class WP_Theme_JSON { + 'styles' => array( + 'border' => array( + 'radius' => null, ++ 'color' => null, ++ 'style' => null, ++ 'width' => null, + ), + 'color' => array( + 'background' => null, +@@ -120,6 +137,9 @@ class WP_Theme_JSON { + 'settings' => array( + 'border' => array( + 'customRadius' => null, ++ 'customColor' => null, ++ 'customStyle' => null, ++ 'customWidth' => null, + ), + 'color' => array( + 'custom' => null, +@@ -165,7 +185,7 @@ class WP_Theme_JSON { + * + * This contains the necessary metadata to process them: + * +- * - path => where to find the preset in a theme.json context ++ * - path => where to find the preset within the settings section + * + * - value_key => the key that represents the value + * +@@ -182,7 +202,7 @@ class WP_Theme_JSON { + */ + const PRESETS_METADATA = array( + array( +- 'path' => array( 'settings', 'color', 'palette' ), ++ 'path' => array( 'color', 'palette' ), + 'value_key' => 'color', + 'css_var_infix' => 'color', + 'classes' => array( +@@ -197,7 +217,7 @@ class WP_Theme_JSON { + ), + ), + array( +- 'path' => array( 'settings', 'color', 'gradients' ), ++ 'path' => array( 'color', 'gradients' ), + 'value_key' => 'gradient', + 'css_var_infix' => 'gradient', + 'classes' => array( +@@ -208,7 +228,7 @@ class WP_Theme_JSON { + ), + ), + array( +- 'path' => array( 'settings', 'typography', 'fontSizes' ), ++ 'path' => array( 'typography', 'fontSizes' ), + 'value_key' => 'size', + 'css_var_infix' => 'font-size', + 'classes' => array( +@@ -219,7 +239,7 @@ class WP_Theme_JSON { + ), + ), + array( +- 'path' => array( 'settings', 'typography', 'fontFamilies' ), ++ 'path' => array( 'typography', 'fontFamilies' ), + 'value_key' => 'fontFamily', + 'css_var_infix' => 'font-family', + 'classes' => array(), +@@ -251,6 +271,18 @@ class WP_Theme_JSON { + 'value' => array( 'border', 'radius' ), + 'support' => array( '__experimentalBorder', 'radius' ), + ), ++ 'borderColor' => array( ++ 'value' => array( 'border', 'color' ), ++ 'support' => array( '__experimentalBorder', 'color' ), ++ ), ++ 'borderWidth' => array( ++ 'value' => array( 'border', 'width' ), ++ 'support' => array( '__experimentalBorder', 'width' ), ++ ), ++ 'borderStyle' => array( ++ 'value' => array( 'border', 'style' ), ++ 'support' => array( '__experimentalBorder', 'style' ), ++ ), + 'color' => array( + 'value' => array( 'color', 'text' ), + 'support' => array( 'color' ), +@@ -293,96 +325,145 @@ class WP_Theme_JSON { + /** + * Constructor. + * +- * @param array $contexts A structure that follows the theme.json schema. +- * @param boolean $should_escape_styles Whether the incoming styles should be escaped. ++ * @param array $theme_json A structure that follows the theme.json schema. + */ +- public function __construct( $contexts = array(), $should_escape_styles = false ) { +- $this->contexts = array(); ++ public function __construct( $theme_json = array() ) { ++ $this->theme_json = array(); + +- if ( ! is_array( $contexts ) ) { ++ if ( ! is_array( $theme_json ) ) { + return; + } + +- $metadata = $this->get_blocks_metadata(); +- foreach ( $contexts as $key => $context ) { +- if ( ! isset( $metadata[ $key ] ) ) { +- // Skip incoming contexts that can't be found +- // within the contexts registered. +- continue; ++ // Remove top-level keys that aren't present in the schema. ++ $this->theme_json = array_intersect_key( $theme_json, self::SCHEMA ); ++ ++ $block_metadata = $this->get_blocks_metadata(); ++ foreach ( array( 'settings', 'styles' ) as $subtree ) { ++ // Remove settings & styles subtrees if they aren't arrays. ++ if ( isset( $this->theme_json[ $subtree ] ) && ! is_array( $this->theme_json[ $subtree ] ) ) { ++ unset( $this->theme_json[ $subtree ] ); ++ } ++ ++ // Remove block selectors subtrees declared within settings & styles if that aren't registered. ++ if ( isset( $this->theme_json[ $subtree ] ) ) { ++ $this->theme_json[ $subtree ] = array_intersect_key( $this->theme_json[ $subtree ], $block_metadata ); + } ++ } ++ ++ foreach ( $block_metadata as $block_selector => $metadata ) { ++ if ( isset( $this->theme_json['styles'][ $block_selector ] ) ) { ++ // Remove the block selector subtree if it's not an array. ++ if ( ! is_array( $this->theme_json['styles'][ $block_selector ] ) ) { ++ unset( $this->theme_json['styles'][ $block_selector ] ); ++ continue; ++ } ++ ++ // Remove the properties the block doesn't support. ++ // This is a subset of the full styles schema. ++ $styles_schema = self::SCHEMA['styles']; ++ foreach ( self::PROPERTIES_METADATA as $prop_name => $prop_meta ) { ++ if ( ! in_array( $prop_name, $metadata['supports'], true ) ) { ++ unset( $styles_schema[ $prop_meta['value'][0] ][ $prop_meta['value'][1] ] ); ++ } ++ } ++ self::remove_keys_not_in_schema( ++ $this->theme_json['styles'][ $block_selector ], ++ $styles_schema ++ ); + +- // Filter out top-level keys that aren't valid according to the schema. +- $context = array_intersect_key( $context, self::SCHEMA ); +- +- // Process styles subtree. +- $this->process_key( 'styles', $context, self::SCHEMA ); +- if ( isset( $context['styles'] ) ) { +- $this->process_key( 'border', $context['styles'], self::SCHEMA['styles'], $should_escape_styles ); +- $this->process_key( 'color', $context['styles'], self::SCHEMA['styles'], $should_escape_styles ); +- $this->process_key( 'spacing', $context['styles'], self::SCHEMA['styles'], $should_escape_styles ); +- $this->process_key( 'typography', $context['styles'], self::SCHEMA['styles'], $should_escape_styles ); +- +- if ( empty( $context['styles'] ) ) { +- unset( $context['styles'] ); +- } else { +- $this->contexts[ $key ]['styles'] = $context['styles']; ++ // Remove the block selector subtree if it is empty after having processed it. ++ if ( empty( $this->theme_json['styles'][ $block_selector ] ) ) { ++ unset( $this->theme_json['styles'][ $block_selector ] ); + } + } + +- // Process settings subtree. +- $this->process_key( 'settings', $context, self::SCHEMA ); +- if ( isset( $context['settings'] ) ) { +- $this->process_key( 'border', $context['settings'], self::SCHEMA['settings'] ); +- $this->process_key( 'color', $context['settings'], self::SCHEMA['settings'] ); +- $this->process_key( 'spacing', $context['settings'], self::SCHEMA['settings'] ); +- $this->process_key( 'typography', $context['settings'], self::SCHEMA['settings'] ); +- +- if ( empty( $context['settings'] ) ) { +- unset( $context['settings'] ); +- } else { +- $this->contexts[ $key ]['settings'] = $context['settings']; ++ if ( isset( $this->theme_json['settings'][ $block_selector ] ) ) { ++ // Remove the block selector subtree if it's not an array. ++ if ( ! is_array( $this->theme_json['settings'][ $block_selector ] ) ) { ++ unset( $this->theme_json['settings'][ $block_selector ] ); ++ continue; ++ } ++ ++ // Remove the properties that aren't present in the schema. ++ self::remove_keys_not_in_schema( ++ $this->theme_json['settings'][ $block_selector ], ++ self::SCHEMA['settings'] ++ ); ++ ++ // Remove the block selector subtree if it is empty after having processed it. ++ if ( empty( $this->theme_json['settings'][ $block_selector ] ) ) { ++ unset( $this->theme_json['settings'][ $block_selector ] ); + } + } + } ++ ++ // Remove the settings & styles subtrees if they're empty after having processed them. ++ foreach ( array( 'settings', 'styles' ) as $subtree ) { ++ if ( empty( $this->theme_json[ $subtree ] ) ) { ++ unset( $this->theme_json[ $subtree ] ); ++ } ++ } ++ ++ } ++ ++ /** ++ * Returns the kebab-cased name of a given property. ++ * ++ * @param string $property Property name to convert. ++ * @return string kebab-cased name of the property ++ */ ++ private static function to_kebab_case( $property ) { ++ $mappings = self::get_case_mappings(); ++ return $mappings['to_kebab_case'][ $property ]; ++ } ++ ++ /** ++ * Returns the property name of a kebab-cased property. ++ * ++ * @param string $property Property name to convert in kebab-case. ++ * @return string Name of the property ++ */ ++ private static function to_property( $property ) { ++ $mappings = self::get_case_mappings(); ++ return $mappings['to_property'][ $property ]; + } + + /** + * Returns a mapping on metadata properties to avoid having to constantly + * transforms properties between camel case and kebab. + * +- * @return array Containing three mappings +- * "to_kebab_case" mapping properties in camel case to ++ * @return array Containing two mappings: ++ * ++ * - "to_kebab_case" mapping properties in camel case to + * properties in kebab case e.g: "paddingTop" to "padding-top". +- * "to_camel_case" mapping properties in kebab case to +- * properties in camel case e.g: "padding-top" to "paddingTop". +- * "to_property" mapping properties in kebab case to ++ * ++ * - "to_property" mapping properties in kebab case to + * the main properties in camel case e.g: "padding-top" to "padding". + */ +- private static function get_properties_metadata_case_mappings() { +- static $properties_metadata_case_mappings; +- if ( null === $properties_metadata_case_mappings ) { +- $properties_metadata_case_mappings = array( ++ private static function get_case_mappings() { ++ static $case_mappings; ++ if ( null === $case_mappings ) { ++ $case_mappings = array( + 'to_kebab_case' => array(), +- 'to_camel_case' => array(), + 'to_property' => array(), + ); + foreach ( self::PROPERTIES_METADATA as $key => $metadata ) { + $kebab_case = strtolower( preg_replace( '/(? array( +- 'selector' => self::GLOBAL_SELECTOR, +- 'supports' => self::GLOBAL_SUPPORTS, ++ self::ROOT_BLOCK_NAME => array( ++ 'selector' => self::ROOT_BLOCK_SELECTOR, ++ 'supports' => self::ROOT_BLOCK_SUPPORTS, ++ ), ++ // By make supports an empty array ++ // this won't have any styles associated ++ // but still allows adding settings ++ // and generate presets. ++ self::ALL_BLOCKS_NAME => array( ++ 'selector' => self::ALL_BLOCKS_SELECTOR, ++ 'supports' => array(), + ), + ); + +@@ -500,83 +589,25 @@ class WP_Theme_JSON { + } + + /** +- * Normalize the subtree according to the given schema. +- * This function modifies the given input by removing +- * the nodes that aren't valid per the schema. +- * +- * @param string $key Key of the subtree to normalize. +- * @param array $input Whole tree to normalize. +- * @param array $schema Schema to use for normalization. +- * @param boolean $should_escape Whether the subproperties should be escaped. ++ * Given a tree, removes the keys that are not present in the schema. ++ * ++ * It is recursive and modifies the input in-place. ++ * ++ * @param array $tree Input to process. ++ * @param array $schema Schema to adhere to. + */ +- private static function process_key( $key, &$input, $schema, $should_escape = false ) { +- if ( ! isset( $input[ $key ] ) ) { +- return; +- } +- +- // Consider valid the input value. +- if ( null === $schema[ $key ] ) { +- return; +- } +- +- if ( ! is_array( $input[ $key ] ) ) { +- unset( $input[ $key ] ); +- return; +- } ++ private static function remove_keys_not_in_schema( &$tree, $schema ) { ++ $tree = array_intersect_key( $tree, $schema ); + +- $input[ $key ] = array_intersect_key( +- $input[ $key ], +- $schema[ $key ] +- ); ++ foreach ( $schema as $key => $data ) { ++ if ( is_array( $schema[ $key ] ) && isset( $tree[ $key ] ) ) { ++ self::remove_keys_not_in_schema( $tree[ $key ], $schema[ $key ] ); + +- if ( $should_escape ) { +- $subtree = $input[ $key ]; +- foreach ( $subtree as $property => $value ) { +- $name = 'background-color'; +- if ( 'gradient' === $property ) { +- $name = 'background'; +- } +- +- if ( is_array( $value ) ) { +- $result = array(); +- foreach ( $value as $subproperty => $subvalue ) { +- $result_subproperty = safecss_filter_attr( "$name: $subvalue" ); +- if ( '' !== $result_subproperty ) { +- $result[ $subproperty ] = $result_subproperty; +- } +- } +- +- if ( empty( $result ) ) { +- unset( $input[ $key ][ $property ] ); +- } +- } else { +- $result = safecss_filter_attr( "$name: $value" ); +- +- if ( '' === $result ) { +- unset( $input[ $key ][ $property ] ); +- } ++ if ( empty( $tree[ $key ] ) ) { ++ unset( $tree[ $key ] ); + } + } + } +- +- if ( 0 === count( $input[ $key ] ) ) { +- unset( $input[ $key ] ); +- } +- } +- +- /** +- * Given a context, it returns its settings subtree. +- * +- * @param array $context Context adhering to the theme.json schema. +- * +- * @return array|null The settings subtree. +- */ +- private static function extract_settings( $context ) { +- if ( empty( $context['settings'] ) ) { +- return null; +- } +- +- return $context['settings']; + } + + /** +@@ -688,7 +719,7 @@ class WP_Theme_JSON { + } + + /** +- * Given a context, it extracts the style properties ++ * Given a styles array, it extracts the style properties + * and adds them to the $declarations array following the format: + * + * ```php +@@ -700,23 +731,24 @@ class WP_Theme_JSON { + * + * Note that this modifies the $declarations in place. + * +- * @param array $declarations Holds the existing declarations. +- * @param array $context Input context to process. +- * @param array $context_supports Supports information for this context. ++ * @param array $declarations Holds the existing declarations. ++ * @param array $styles Styles to process. ++ * @param array $supports Supports information for this block. + */ +- private static function compute_style_properties( &$declarations, $context, $context_supports ) { +- if ( empty( $context['styles'] ) ) { ++ private static function compute_style_properties( &$declarations, $styles, $supports ) { ++ if ( empty( $styles ) ) { + return; + } +- $metadata_mappings = self::get_properties_metadata_case_mappings(); +- $properties = array(); ++ ++ $properties = array(); + foreach ( self::PROPERTIES_METADATA as $name => $metadata ) { +- if ( ! in_array( $name, $context_supports, true ) ) { ++ if ( ! in_array( $name, $supports, true ) ) { + continue; + } + + // Some properties can be shorthand properties, meaning that + // they contain multiple values instead of a single one. ++ // An example of this is the padding property, see self::SCHEMA. + if ( self::has_properties( $metadata ) ) { + foreach ( $metadata['properties'] as $property ) { + $properties[] = array( +@@ -733,9 +765,9 @@ class WP_Theme_JSON { + } + + foreach ( $properties as $prop ) { +- $value = self::get_property_value( $context['styles'], $prop['value'] ); ++ $value = self::get_property_value( $styles, $prop['value'] ); + if ( ! empty( $value ) ) { +- $kebab_cased_name = $metadata_mappings['to_kebab_case'][ $prop['name'] ]; ++ $kebab_cased_name = self::to_kebab_case( $prop['name'] ); + $declarations[] = array( + 'name' => $kebab_cased_name, + 'value' => $value, +@@ -745,24 +777,24 @@ class WP_Theme_JSON { + } + + /** +- * Given a context, it extracts its presets ++ * Given a settings array, it extracts its presets + * and adds them to the given input $stylesheet. + * + * Note this function modifies $stylesheet in place. + * + * @param string $stylesheet Input stylesheet to add the presets to. +- * @param array $context Context to process. ++ * @param array $settings Settings to process. + * @param string $selector Selector wrapping the classes. + */ +- private static function compute_preset_classes( &$stylesheet, $context, $selector ) { +- if ( self::GLOBAL_SELECTOR === $selector ) { ++ private static function compute_preset_classes( &$stylesheet, $settings, $selector ) { ++ if ( self::ROOT_BLOCK_SELECTOR === $selector ) { + // Classes at the global level do not need any CSS prefixed, + // and we don't want to increase its specificity. + $selector = ''; + } + + foreach ( self::PRESETS_METADATA as $preset ) { +- $values = gutenberg_experimental_get( $context, $preset['path'], array() ); ++ $values = gutenberg_experimental_get( $settings, $preset['path'], array() ); + foreach ( $values as $value ) { + foreach ( $preset['classes'] as $class ) { + $stylesheet .= self::to_ruleset( +@@ -780,7 +812,7 @@ class WP_Theme_JSON { + } + + /** +- * Given a context, it extracts the CSS Custom Properties ++ * Given the block settings, it extracts the CSS Custom Properties + * for the presets and adds them to the $declarations array + * following the format: + * +@@ -794,11 +826,11 @@ class WP_Theme_JSON { + * Note that this modifies the $declarations in place. + * + * @param array $declarations Holds the existing declarations. +- * @param array $context Input context to process. ++ * @param array $settings Settings to process. + */ +- private static function compute_preset_vars( &$declarations, $context ) { ++ private static function compute_preset_vars( &$declarations, $settings ) { + foreach ( self::PRESETS_METADATA as $preset ) { +- $values = gutenberg_experimental_get( $context, $preset['path'], array() ); ++ $values = gutenberg_experimental_get( $settings, $preset['path'], array() ); + foreach ( $values as $value ) { + $declarations[] = array( + 'name' => '--wp--preset--' . $preset['css_var_infix'] . '--' . $value['slug'], +@@ -809,7 +841,7 @@ class WP_Theme_JSON { + } + + /** +- * Given a context, it extracts the CSS Custom Properties ++ * Given an array of settings, it extracts the CSS Custom Properties + * for the custom values and adds them to the $declarations + * array following the format: + * +@@ -823,10 +855,10 @@ class WP_Theme_JSON { + * Note that this modifies the $declarations in place. + * + * @param array $declarations Holds the existing declarations. +- * @param array $context Input context to process. ++ * @param array $settings Settings to process. + */ +- private static function compute_theme_vars( &$declarations, $context ) { +- $custom_values = gutenberg_experimental_get( $context, array( 'settings', 'custom' ) ); ++ private static function compute_theme_vars( &$declarations, $settings ) { ++ $custom_values = gutenberg_experimental_get( $settings, array( 'custom' ) ); + $css_vars = self::flatten_tree( $custom_values ); + foreach ( $css_vars as $key => $value ) { + $declarations[] = array( +@@ -876,15 +908,15 @@ class WP_Theme_JSON { + } + + /** +- * Converts each context into a list of rulesets ++ * Converts each styles section into a list of rulesets + * to be appended to the stylesheet. + * These rulesets contain all the css variables (custom variables and preset variables). + * + * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax + * +- * For each context this creates a new ruleset such as: ++ * For each section this creates a new ruleset such as: + * +- * context-selector { ++ * block-selector { + * --wp--preset--category--slug: value; + * --wp--custom--variable: value; + * } +@@ -893,16 +925,20 @@ class WP_Theme_JSON { + */ + private function get_css_variables() { + $stylesheet = ''; +- $metadata = $this->get_blocks_metadata(); +- foreach ( $this->contexts as $context_name => $context ) { +- if ( empty( $metadata[ $context_name ]['selector'] ) ) { ++ if ( ! isset( $this->theme_json['settings'] ) ) { ++ return $stylesheet; ++ } ++ ++ $metadata = $this->get_blocks_metadata(); ++ foreach ( $this->theme_json['settings'] as $block_selector => $settings ) { ++ if ( empty( $metadata[ $block_selector ]['selector'] ) ) { + continue; + } +- $selector = $metadata[ $context_name ]['selector']; ++ $selector = $metadata[ $block_selector ]['selector']; + + $declarations = array(); +- self::compute_preset_vars( $declarations, $context ); +- self::compute_theme_vars( $declarations, $context ); ++ self::compute_preset_vars( $declarations, $settings ); ++ self::compute_theme_vars( $declarations, $settings ); + + // Attach the ruleset for style and custom properties. + $stylesheet .= self::to_ruleset( $selector, $declarations ); +@@ -911,14 +947,14 @@ class WP_Theme_JSON { + } + + /** +- * Converts each context into a list of rulesets ++ * Converts each style section into a list of rulesets + * containing the block styles to be appended to the stylesheet. + * + * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax + * +- * For each context this creates a new ruleset such as: ++ * For each section this creates a new ruleset such as: + * +- * context-selector { ++ * block-selector { + * style-property-one: value; + * } + * +@@ -949,33 +985,50 @@ class WP_Theme_JSON { + */ + private function get_block_styles() { + $stylesheet = ''; +- $metadata = $this->get_blocks_metadata(); +- foreach ( $this->contexts as $context_name => $context ) { +- if ( empty( $metadata[ $context_name ]['selector'] ) || empty( $metadata[ $context_name ]['supports'] ) ) { ++ if ( ! isset( $this->theme_json['styles'] ) && ! isset( $this->theme_json['settings'] ) ) { ++ return $stylesheet; ++ } ++ ++ $metadata = $this->get_blocks_metadata(); ++ foreach ( $metadata as $block_selector => $metadata ) { ++ if ( empty( $metadata['selector'] ) || empty( $metadata['supports'] ) ) { + continue; + } +- $selector = $metadata[ $context_name ]['selector']; +- $supports = $metadata[ $context_name ]['supports']; ++ ++ $selector = $metadata['selector']; ++ $supports = $metadata['supports']; + + $declarations = array(); +- self::compute_style_properties( $declarations, $context, $supports ); ++ if ( isset( $this->theme_json['styles'][ $block_selector ] ) ) { ++ self::compute_style_properties( ++ $declarations, ++ $this->theme_json['styles'][ $block_selector ], ++ $supports ++ ); ++ } + + $stylesheet .= self::to_ruleset( $selector, $declarations ); + + // Attach the rulesets for the classes. +- self::compute_preset_classes( $stylesheet, $context, $selector ); ++ if ( isset( $this->theme_json['settings'][ $block_selector ] ) ) { ++ self::compute_preset_classes( ++ $stylesheet, ++ $this->theme_json['settings'][ $block_selector ], ++ $selector ++ ); ++ } + } + + return $stylesheet; + } + + /** +- * Returns the existing settings for each context. ++ * Returns the existing settings for each block. + * + * Example: + * + * { +- * 'global': { ++ * 'root': { + * 'color': { + * 'custom': true + * } +@@ -987,15 +1040,14 @@ class WP_Theme_JSON { + * } + * } + * +- * @return array Settings per context. ++ * @return array Settings per block. + */ + public function get_settings() { +- return array_filter( +- array_map( array( $this, 'extract_settings' ), $this->contexts ), +- function ( $element ) { +- return null !== $element; +- } +- ); ++ if ( ! isset( $this->theme_json['settings'] ) ) { ++ return array(); ++ } else { ++ return $this->theme_json['settings']; ++ } + } + + /** +@@ -1019,37 +1071,41 @@ class WP_Theme_JSON { + /** + * Merge new incoming data. + * +- * @param WP_Theme_JSON $theme_json Data to merge. ++ * @param WP_Theme_JSON $incoming Data to merge. + */ +- public function merge( $theme_json ) { +- $incoming_data = $theme_json->get_raw_data(); +- +- foreach ( array_keys( $incoming_data ) as $context ) { +- foreach ( array( 'settings', 'styles' ) as $subtree ) { +- if ( ! isset( $incoming_data[ $context ][ $subtree ] ) ) { +- continue; +- } +- +- if ( ! isset( $this->contexts[ $context ][ $subtree ] ) ) { +- $this->contexts[ $context ][ $subtree ] = $incoming_data[ $context ][ $subtree ]; +- continue; +- } +- +- foreach ( array_keys( self::SCHEMA[ $subtree ] ) as $leaf ) { +- if ( ! isset( $incoming_data[ $context ][ $subtree ][ $leaf ] ) ) { +- continue; +- } +- +- if ( ! isset( $this->contexts[ $context ][ $subtree ][ $leaf ] ) ) { +- $this->contexts[ $context ][ $subtree ][ $leaf ] = $incoming_data[ $context ][ $subtree ][ $leaf ]; +- continue; +- } +- +- $this->contexts[ $context ][ $subtree ][ $leaf ] = array_merge( +- $this->contexts[ $context ][ $subtree ][ $leaf ], +- $incoming_data[ $context ][ $subtree ][ $leaf ] +- ); +- } ++ public function merge( $incoming ) { ++ $incoming_data = $incoming->get_raw_data(); ++ $this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data ); ++ ++ // The array_replace_recursive algorithm merges at the leaf level. ++ // This means that when a leaf value is an array, ++ // the incoming array won't replace the existing, ++ // but the numeric indexes are used for replacement. ++ // ++ // These are the cases that have array values at the leaf levels. ++ $block_metadata = self::get_blocks_metadata(); ++ foreach ( $block_metadata as $block_selector => $meta ) { ++ // Color presets: palette & gradients. ++ if ( isset( $incoming_data['settings'][ $block_selector ]['color']['palette'] ) ) { ++ $this->theme_json['settings'][ $block_selector ]['color']['palette'] = $incoming_data['settings'][ $block_selector ]['color']['palette']; ++ } ++ if ( isset( $incoming_data['settings'][ $block_selector ]['color']['gradients'] ) ) { ++ $this->theme_json['settings'][ $block_selector ]['color']['gradients'] = $incoming_data['settings'][ $block_selector ]['color']['gradients']; ++ } ++ // Spacing: units. ++ if ( isset( $incoming_data['settings'][ $block_selector ]['spacing']['units'] ) ) { ++ $this->theme_json['settings'][ $block_selector ]['spacing']['units'] = $incoming_data['settings'][ $block_selector ]['spacing']['units']; ++ } ++ // Typography presets: fontSizes & fontFamilies. ++ if ( isset( $incoming_data['settings'][ $block_selector ]['typography']['fontSizes'] ) ) { ++ $this->theme_json['settings'][ $block_selector ]['typography']['fontSizes'] = $incoming_data['settings'][ $block_selector ]['typography']['fontSizes']; ++ } ++ if ( isset( $incoming_data['settings'][ $block_selector ]['typography']['fontFamilies'] ) ) { ++ $this->theme_json['settings'][ $block_selector ]['typography']['fontFamilies'] = $incoming_data['settings'][ $block_selector ]['typography']['fontFamilies']; ++ } ++ // Custom section. ++ if ( isset( $incoming_data['settings'][ $block_selector ]['custom'] ) ) { ++ $this->theme_json['settings'][ $block_selector ]['custom'] = $incoming_data['settings'][ $block_selector ]['custom']; + } + } + } +@@ -1058,55 +1114,42 @@ class WP_Theme_JSON { + * Removes insecure data from theme.json. + */ + public function remove_insecure_properties() { +- $blocks_metadata = self::get_blocks_metadata(); +- $metadata_mappings = self::get_properties_metadata_case_mappings(); +- foreach ( $this->contexts as $context_name => &$context ) { +- // Escape the context key. +- if ( empty( $blocks_metadata[ $context_name ] ) ) { +- unset( $this->contexts[ $context_name ] ); +- continue; +- } +- +- $escaped_settings = null; +- $escaped_styles = null; ++ $blocks_metadata = self::get_blocks_metadata(); ++ foreach ( $blocks_metadata as $block_selector => $metadata ) { ++ $escaped_settings = array(); ++ $escaped_styles = array(); + + // Style escaping. +- if ( ! empty( $context['styles'] ) ) { +- $supports = $blocks_metadata[ $context_name ]['supports']; ++ if ( isset( $this->theme_json['styles'][ $block_selector ] ) ) { + $declarations = array(); +- self::compute_style_properties( $declarations, $context, $supports ); ++ self::compute_style_properties( $declarations, $this->theme_json['styles'][ $block_selector ], $metadata['supports'] ); + foreach ( $declarations as $declaration ) { + $style_to_validate = $declaration['name'] . ': ' . $declaration['value']; + if ( esc_html( safecss_filter_attr( $style_to_validate ) ) === $style_to_validate ) { +- if ( null === $escaped_styles ) { +- $escaped_styles = array(); +- } +- $property = $metadata_mappings['to_property'][ $declaration['name'] ]; ++ $property = self::to_property( $declaration['name'] ); + $path = self::PROPERTIES_METADATA[ $property ]['value']; + if ( self::has_properties( self::PROPERTIES_METADATA[ $property ] ) ) { + $declaration_divided = explode( '-', $declaration['name'] ); + $path[] = $declaration_divided[1]; +- gutenberg_experimental_set( +- $escaped_styles, +- $path, +- gutenberg_experimental_get( $context['styles'], $path ) +- ); +- } else { +- gutenberg_experimental_set( +- $escaped_styles, +- $path, +- gutenberg_experimental_get( $context['styles'], $path ) +- ); + } ++ gutenberg_experimental_set( ++ $escaped_styles, ++ $path, ++ gutenberg_experimental_get( $this->theme_json['styles'][ $block_selector ], $path ) ++ ); + } + } + } + + // Settings escaping. + // For now the ony allowed settings are presets. +- if ( ! empty( $context['settings'] ) ) { ++ if ( isset( $this->theme_json['settings'][ $block_selector ] ) ) { + foreach ( self::PRESETS_METADATA as $preset_metadata ) { +- $current_preset = gutenberg_experimental_get( $context, $preset_metadata['path'], null ); ++ $current_preset = gutenberg_experimental_get( ++ $this->theme_json['settings'][ $block_selector ], ++ $preset_metadata['path'], ++ null ++ ); + if ( null !== $current_preset ) { + $escaped_preset = array(); + foreach ( $current_preset as $single_preset ) { +@@ -1136,34 +1179,23 @@ class WP_Theme_JSON { + } + } + } +- if ( count( $escaped_preset ) > 0 ) { +- if ( null === $escaped_settings ) { +- $escaped_settings = array(); +- } ++ if ( ! empty( $escaped_preset ) ) { + gutenberg_experimental_set( $escaped_settings, $preset_metadata['path'], $escaped_preset ); + } + } + } +- if ( null !== $escaped_settings ) { +- $escaped_settings = $escaped_settings['settings']; +- } + } + +- if ( null === $escaped_settings && null === $escaped_styles ) { +- unset( $this->contexts[ $context_name ] ); +- } elseif ( null !== $escaped_settings && null !== $escaped_styles ) { +- $context = array( +- 'styles' => $escaped_styles, +- 'settings' => $escaped_settings, +- ); +- } elseif ( null === $escaped_settings ) { +- $context = array( +- 'styles' => $escaped_styles, +- ); ++ if ( empty( $escaped_settings ) ) { ++ unset( $this->theme_json['settings'][ $block_selector ] ); + } else { +- $context = array( +- 'settings' => $escaped_settings, +- ); ++ $this->theme_json['settings'][ $block_selector ] = $escaped_settings; ++ } ++ ++ if ( empty( $escaped_styles ) ) { ++ unset( $this->theme_json['styles'][ $block_selector ] ); ++ } else { ++ $this->theme_json['styles'][ $block_selector ] = $escaped_styles; + } + } + } +@@ -1174,7 +1206,7 @@ class WP_Theme_JSON { + * @return array Raw data. + */ + public function get_raw_data() { +- return $this->contexts; ++ return $this->theme_json; + } + + } +diff --git a/lib/client-assets.php b/lib/client-assets.php +index a8af2b2b34..4fb94d05b9 100644 +--- a/lib/client-assets.php ++++ b/lib/client-assets.php +@@ -88,11 +88,11 @@ function gutenberg_override_script( $scripts, $handle, $src, $deps = array(), $v + * `WP_Dependencies::set_translations` will fall over on itself if setting + * translations on the `wp-i18n` handle, since it internally adds `wp-i18n` + * as a dependency of itself, exhausting memory. The same applies for the +- * polyfill script, which is a dependency _of_ `wp-i18n`. ++ * polyfill and hooks scripts, which are dependencies _of_ `wp-i18n`. + * + * See: https://core.trac.wordpress.org/ticket/46089 + */ +- if ( 'wp-i18n' !== $handle && 'wp-polyfill' !== $handle ) { ++ if ( ! in_array( $handle, array( 'wp-i18n', 'wp-polyfill', 'wp-hooks' ), true ) ) { + $scripts->set_translations( $handle, 'default' ); + } + if ( 'wp-i18n' === $handle ) { +@@ -316,7 +316,7 @@ function gutenberg_register_packages_styles( $styles ) { + $styles, + 'wp-block-editor', + gutenberg_url( 'build/block-editor/style.css' ), +- array( 'wp-components', 'wp-editor-font' ), ++ array( 'wp-components' ), + filemtime( gutenberg_dir_path() . 'build/editor/style.css' ) + ); + $styles->add_data( 'wp-block-editor', 'rtl', 'replace' ); +@@ -695,7 +695,7 @@ function gutenberg_extend_block_editor_styles_html() { + + $block_registry = WP_Block_Type_Registry::get_instance(); + +- foreach ( $block_registry->get_all_registered() as $block_name => $block_type ) { ++ foreach ( $block_registry->get_all_registered() as $block_type ) { + if ( ! empty( $block_type->style ) ) { + $handles[] = $block_type->style; + } +diff --git a/lib/editor-settings.php b/lib/editor-settings.php +index e0970e761b..e50141f1f3 100644 +--- a/lib/editor-settings.php ++++ b/lib/editor-settings.php +@@ -45,6 +45,7 @@ function gutenberg_get_common_block_editor_settings() { + 'disableCustomGradients' => get_theme_support( 'disable-custom-gradients' ), + 'enableCustomLineHeight' => get_theme_support( 'custom-line-height' ), + 'enableCustomUnits' => get_theme_support( 'custom-units' ), ++ 'enableCustomSpacing' => get_theme_support( 'custom-spacing' ), + 'imageSizes' => $available_image_sizes, + 'isRTL' => is_rtl(), + 'maxUploadFileSize' => $max_upload_size, +diff --git a/lib/experimental-default-theme.json b/lib/experimental-default-theme.json +index 1ad199567d..da910ca847 100644 +--- a/lib/experimental-default-theme.json ++++ b/lib/experimental-default-theme.json +@@ -1,6 +1,6 @@ + { +- "global": { +- "settings": { ++ "settings": { ++ "defaults": { + "color": { + "palette": [ + { +diff --git a/lib/experimental-i18n-theme.json b/lib/experimental-i18n-theme.json +index 4731a59df0..cd8d4f2c76 100644 +--- a/lib/experimental-i18n-theme.json ++++ b/lib/experimental-i18n-theme.json +@@ -1,32 +1,18 @@ + { + "settings": { +- "typography": { +- "fontSizes": [ +- "name" +- ], +- "fontStyles": [ +- "name" +- ], +- "fontWeights": [ +- "name" +- ], +- "fontFamilies": [ +- "name" +- ], +- "textTransforms": [ +- "name" +- ], +- "textDecorations": [ +- "name" +- ] +- }, +- "color": { +- "palette": [ +- "name" +- ], +- "gradients": [ +- "name" +- ] ++ "*": { ++ "typography": { ++ "fontSizes": [ "name" ], ++ "fontStyles": [ "name" ], ++ "fontWeights": [ "name" ], ++ "fontFamilies": [ "name" ], ++ "textTransforms": [ "name" ], ++ "textDecorations": [ "name" ] ++ }, ++ "color": { ++ "palette": [ "name" ], ++ "gradients": [ "name" ] ++ } + } + } + } +diff --git a/lib/full-site-editing/block-templates.php b/lib/full-site-editing/block-templates.php +index bb5d26157f..5f706c2233 100644 +--- a/lib/full-site-editing/block-templates.php ++++ b/lib/full-site-editing/block-templates.php +@@ -105,6 +105,40 @@ function _gutenberg_get_template_files( $template_type ) { + return $template_files; + } + ++/** ++ * Parses wp_template content and injects the current theme's ++ * stylesheet as a theme attribute into each wp_template_part ++ * ++ * @param string $template_content serialized wp_template content. ++ * ++ * @return string Updated wp_template content. ++ */ ++function _inject_theme_attribute_in_content( $template_content ) { ++ $has_updated_content = false; ++ $new_content = ''; ++ $template_blocks = parse_blocks( $template_content ); ++ ++ foreach ( $template_blocks as $key => $block ) { ++ if ( ++ 'core/template-part' === $block['blockName'] && ++ ! isset( $block['attrs']['theme'] ) ++ ) { ++ $template_blocks[ $key ]['attrs']['theme'] = wp_get_theme()->get_stylesheet(); ++ $has_updated_content = true; ++ } ++ } ++ ++ if ( $has_updated_content ) { ++ foreach ( $template_blocks as $block ) { ++ $new_content .= serialize_block( $block ); ++ } ++ ++ return $new_content; ++ } ++ ++ return $template_content; ++} ++ + /** + * Build a unified template object based on a theme file. + * +@@ -115,12 +149,17 @@ function _gutenberg_get_template_files( $template_type ) { + */ + function _gutenberg_build_template_result_from_file( $template_file, $template_type ) { + $default_template_types = gutenberg_get_default_template_types(); ++ $template_content = file_get_contents( $template_file['path'] ); ++ $theme = wp_get_theme()->get_stylesheet(); ++ ++ if ( 'wp_template' === $template_type ) { ++ $template_content = _inject_theme_attribute_in_content( $template_content ); ++ } + +- $theme = wp_get_theme()->get_stylesheet(); + $template = new WP_Block_Template(); + $template->id = $theme . '//' . $template_file['slug']; + $template->theme = $theme; +- $template->content = file_get_contents( $template_file['path'] ); ++ $template->content = $template_content; + $template->slug = $template_file['slug']; + $template->is_custom = false; + $template->type = $template_type; +diff --git a/lib/full-site-editing/default-template-types.php b/lib/full-site-editing/default-template-types.php +index 54cc722be9..132aef0224 100644 +--- a/lib/full-site-editing/default-template-types.php ++++ b/lib/full-site-editing/default-template-types.php +@@ -27,7 +27,7 @@ function gutenberg_get_default_template_types() { + ), + 'singular' => array( + 'title' => _x( 'Singular', 'Template name', 'gutenberg' ), +- 'description' => __( 'Used when a single entry is queried. This template will be overridden the Single, Post, and Page templates where appropriate', 'gutenberg' ), ++ 'description' => __( 'Used when a single entry is queried. This template will be overridden by the Single, Post, and Page templates where appropriate', 'gutenberg' ), + ), + 'single' => array( + 'title' => _x( 'Single', 'Template name', 'gutenberg' ), +@@ -43,7 +43,7 @@ function gutenberg_get_default_template_types() { + ), + 'archive' => array( + 'title' => _x( 'Archive', 'Template name', 'gutenberg' ), +- 'description' => __( 'Used when multiple entries are queried. This template will be overridden the Category, Author, and Date templates where appropriate', 'gutenberg' ), ++ 'description' => __( 'Used when multiple entries are queried. This template will be overridden by the Category, Author, and Date templates where appropriate', 'gutenberg' ), + ), + 'author' => array( + 'title' => _x( 'Author Archive', 'Template name', 'gutenberg' ), +diff --git a/lib/full-site-editing/edit-site-export.php b/lib/full-site-editing/edit-site-export.php +index b299b51e16..d22a773525 100644 +--- a/lib/full-site-editing/edit-site-export.php ++++ b/lib/full-site-editing/edit-site-export.php +@@ -6,15 +6,48 @@ + */ + + /** +- * Output a ZIP file with an export of the current templates +- * and template parts from the site editor, and close the connection. ++ * Parses wp_template content and removes the theme attribute from ++ * each wp_template_part ++ * ++ * @param string $template_content serialized wp_template content. ++ * ++ * @return string Updated wp_template content. + */ +-function gutenberg_edit_site_export() { +- // Create ZIP file and directories. +- $filename = tempnam( get_temp_dir(), 'edit-site-export' ); ++function _remove_theme_attribute_from_content( $template_content ) { ++ $has_updated_content = false; ++ $new_content = ''; ++ $template_blocks = parse_blocks( $template_content ); ++ ++ foreach ( $template_blocks as $key => $block ) { ++ if ( 'core/template-part' === $block['blockName'] && isset( $block['attrs']['theme'] ) ) { ++ unset( $template_blocks[ $key ]['attrs']['theme'] ); ++ $has_updated_content = true; ++ } ++ } ++ ++ if ( $has_updated_content ) { ++ foreach ( $template_blocks as $block ) { ++ $new_content .= serialize_block( $block ); ++ } ++ ++ return $new_content; ++ } ++ ++ return $template_content; ++} ++ ++/** ++ * Creates an export of the current templates and ++ * template parts from the site editor at the ++ * specified path in a ZIP file. ++ * ++ * @param string $filename path of the ZIP file. ++ */ ++function gutenberg_edit_site_export_create_zip( $filename ) { + if ( ! class_exists( 'ZipArchive' ) ) { + return new WP_Error( 'Zip Export not supported.' ); + } ++ + $zip = new ZipArchive(); + $zip->open( $filename, ZipArchive::OVERWRITE ); + $zip->addEmptyDir( 'theme' ); +@@ -24,6 +57,8 @@ function gutenberg_edit_site_export() { + // Load templates into the zip file. + $templates = gutenberg_get_block_templates(); + foreach ( $templates as $template ) { ++ $template->content = _remove_theme_attribute_from_content( $template->content ); ++ + $zip->addFromString( + 'theme/block-templates/' . $template->slug . '.html', + $template->content +@@ -39,8 +74,19 @@ function gutenberg_edit_site_export() { + ); + } + +- // Send back the ZIP file. ++ // Save changes to the zip file. + $zip->close(); ++} ++ ++/** ++ * Output a ZIP file with an export of the current templates ++ * and template parts from the site editor, and close the connection. ++ */ ++function gutenberg_edit_site_export() { ++ // Create ZIP file in the temporary directory. ++ $filename = tempnam( get_temp_dir(), 'edit-site-export' ); ++ gutenberg_edit_site_export_create_zip( $filename ); ++ + header( 'Content-Type: application/zip' ); + header( 'Content-Disposition: attachment; filename=edit-site-export.zip' ); + header( 'Content-Length: ' . filesize( $filename ) ); +diff --git a/lib/full-site-editing/edit-site-page.php b/lib/full-site-editing/edit-site-page.php +index d73ad831a5..992e9b82a0 100644 +--- a/lib/full-site-editing/edit-site-page.php ++++ b/lib/full-site-editing/edit-site-page.php +@@ -50,7 +50,7 @@ function gutenberg_get_editor_styles() { + ); + + /* translators: Use this to specify the CSS font family for the default font. */ +- $locale_font_family = esc_html_x( 'Noto Serif', 'CSS Font Family for Editor Font', 'gutenberg' ); ++ $locale_font_family = '-apple-system, BlinkMacSystemFont,"Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell,"Helvetica Neue", sans-serif'; + $styles[] = array( + 'css' => "body { font-family: '$locale_font_family' }", + ); +diff --git a/lib/full-site-editing/template-loader.php b/lib/full-site-editing/template-loader.php +index 1ffdec27a3..e0047b81c2 100644 +--- a/lib/full-site-editing/template-loader.php ++++ b/lib/full-site-editing/template-loader.php +@@ -154,15 +154,14 @@ function gutenberg_render_title_tag() { + } + + /** +- * Renders the markup for the current template. ++ * Returns the markup for the current template. + */ +-function gutenberg_render_the_template() { ++function gutenberg_get_the_template_html() { + global $_wp_current_template_content; + global $wp_embed; + + if ( ! $_wp_current_template_content ) { +- echo '

' . esc_html__( 'No matching template found', 'gutenberg' ) . '

'; +- return; ++ return '

' . esc_html__( 'No matching template found', 'gutenberg' ) . '

'; + } + + $content = $wp_embed->run_shortcode( $_wp_current_template_content ); +@@ -178,9 +177,14 @@ function gutenberg_render_the_template() { + + // Wrap block template in .wp-site-blocks to allow for specific descendant styles + // (e.g. `.wp-site-blocks > *`). +- echo '
'; +- echo $content; // phpcs:ignore WordPress.Security.EscapeOutput +- echo '
'; ++ return '
' . $content . '
'; ++} ++ ++/** ++ * Renders the markup for the current template. ++ */ ++function gutenberg_render_the_template() { ++ echo gutenberg_get_the_template_html(); // phpcs:ignore WordPress.Security.EscapeOutput + } + + /** +diff --git a/lib/global-styles.php b/lib/global-styles.php +index 5aab927771..697d2cf766 100644 +--- a/lib/global-styles.php ++++ b/lib/global-styles.php +@@ -22,60 +22,61 @@ function gutenberg_experimental_global_styles_has_theme_json_support() { + * @return array Config that adheres to the theme.json schema. + */ + function gutenberg_experimental_global_styles_get_theme_support_settings( $settings ) { +- $theme_settings = array(); +- $theme_settings['global'] = array(); +- $theme_settings['global']['settings'] = array(); ++ $all_blocks = WP_Theme_JSON::ALL_BLOCKS_NAME; ++ $theme_settings = array(); ++ $theme_settings['settings'] = array(); ++ $theme_settings['settings'][ $all_blocks ] = array(); + + // Deprecated theme supports. + if ( isset( $settings['disableCustomColors'] ) ) { +- if ( ! isset( $theme_settings['global']['settings']['color'] ) ) { +- $theme_settings['global']['settings']['color'] = array(); ++ if ( ! isset( $theme_settings['settings'][ $all_blocks ]['color'] ) ) { ++ $theme_settings['settings'][ $all_blocks ]['color'] = array(); + } +- $theme_settings['global']['settings']['color']['custom'] = ! $settings['disableCustomColors']; ++ $theme_settings['settings'][ $all_blocks ]['color']['custom'] = ! $settings['disableCustomColors']; + } + + if ( isset( $settings['disableCustomGradients'] ) ) { +- if ( ! isset( $theme_settings['global']['settings']['color'] ) ) { +- $theme_settings['global']['settings']['color'] = array(); ++ if ( ! isset( $theme_settings['settings'][ $all_blocks ]['color'] ) ) { ++ $theme_settings['settings'][ $all_blocks ]['color'] = array(); + } +- $theme_settings['global']['settings']['color']['customGradient'] = ! $settings['disableCustomGradients']; ++ $theme_settings['settings'][ $all_blocks ]['color']['customGradient'] = ! $settings['disableCustomGradients']; + } + + if ( isset( $settings['disableCustomFontSizes'] ) ) { +- if ( ! isset( $theme_settings['global']['settings']['typography'] ) ) { +- $theme_settings['global']['settings']['typography'] = array(); ++ if ( ! isset( $theme_settings['settings'][ $all_blocks ]['typography'] ) ) { ++ $theme_settings['settings'][ $all_blocks ]['typography'] = array(); + } +- $theme_settings['global']['settings']['typography']['customFontSize'] = ! $settings['disableCustomFontSizes']; ++ $theme_settings['settings'][ $all_blocks ]['typography']['customFontSize'] = ! $settings['disableCustomFontSizes']; + } + + if ( isset( $settings['enableCustomLineHeight'] ) ) { +- if ( ! isset( $theme_settings['global']['settings']['typography'] ) ) { +- $theme_settings['global']['settings']['typography'] = array(); ++ if ( ! isset( $theme_settings['settings'][ $all_blocks ]['typography'] ) ) { ++ $theme_settings['settings'][ $all_blocks ]['typography'] = array(); + } +- $theme_settings['global']['settings']['typography']['customLineHeight'] = $settings['enableCustomLineHeight']; ++ $theme_settings['settings'][ $all_blocks ]['typography']['customLineHeight'] = $settings['enableCustomLineHeight']; + } + + if ( isset( $settings['enableCustomUnits'] ) ) { +- if ( ! isset( $theme_settings['global']['settings']['spacing'] ) ) { +- $theme_settings['global']['settings']['spacing'] = array(); ++ if ( ! isset( $theme_settings['settings'][ $all_blocks ]['spacing'] ) ) { ++ $theme_settings['settings'][ $all_blocks ]['spacing'] = array(); + } +- $theme_settings['global']['settings']['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ? ++ $theme_settings['settings'][ $all_blocks ]['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ? + array( 'px', 'em', 'rem', 'vh', 'vw' ) : + $settings['enableCustomUnits']; + } + + if ( isset( $settings['colors'] ) ) { +- if ( ! isset( $theme_settings['global']['settings']['color'] ) ) { +- $theme_settings['global']['settings']['color'] = array(); ++ if ( ! isset( $theme_settings['settings'][ $all_blocks ]['color'] ) ) { ++ $theme_settings['settings'][ $all_blocks ]['color'] = array(); + } +- $theme_settings['global']['settings']['color']['palette'] = $settings['colors']; ++ $theme_settings['settings'][ $all_blocks ]['color']['palette'] = $settings['colors']; + } + + if ( isset( $settings['gradients'] ) ) { +- if ( ! isset( $theme_settings['global']['settings']['color'] ) ) { +- $theme_settings['global']['settings']['color'] = array(); ++ if ( ! isset( $theme_settings['settings'][ $all_blocks ]['color'] ) ) { ++ $theme_settings['settings'][ $all_blocks ]['color'] = array(); + } +- $theme_settings['global']['settings']['color']['gradients'] = $settings['gradients']; ++ $theme_settings['settings'][ $all_blocks ]['color']['gradients'] = $settings['gradients']; + } + + if ( isset( $settings['fontSizes'] ) ) { +@@ -86,25 +87,34 @@ function gutenberg_experimental_global_styles_get_theme_support_settings( $setti + $font_size['size'] = $font_size['size'] . 'px'; + } + } +- if ( ! isset( $theme_settings['global']['settings']['typography'] ) ) { +- $theme_settings['global']['settings']['typography'] = array(); ++ if ( ! isset( $theme_settings['settings'][ $all_blocks ]['typography'] ) ) { ++ $theme_settings['settings'][ $all_blocks ]['typography'] = array(); + } +- $theme_settings['global']['settings']['typography']['fontSizes'] = $font_sizes; ++ $theme_settings['settings'][ $all_blocks ]['typography']['fontSizes'] = $font_sizes; + } + +- // Things that didn't land in core yet, so didn't have a setting assigned. +- if ( current( (array) get_theme_support( 'custom-spacing' ) ) ) { +- if ( ! isset( $theme_settings['global']['settings']['spacing'] ) ) { +- $theme_settings['global']['settings']['spacing'] = array(); ++ // This allows to make the plugin work with WordPress 5.7 beta ++ // as well as lower versions. The second check can be removed ++ // as soon as the minimum WordPress version for the plugin ++ // is bumped to 5.7. ++ if ( isset( $settings['enableCustomSpacing'] ) ) { ++ if ( ! isset( $theme_settings['settings'][ $all_blocks ]['spacing'] ) ) { ++ $theme_settings['settings'][ $all_blocks ]['spacing'] = array(); ++ } ++ $theme_settings['settings'][ $all_blocks ]['spacing']['customPadding'] = $settings['enableCustomSpacing']; ++ } else if ( current( (array) get_theme_support( 'custom-spacing' ) ) ) { ++ if ( ! isset( $theme_settings['settings'][ $all_blocks ]['spacing'] ) ) { ++ $theme_settings['settings'][ $all_blocks ]['spacing'] = array(); + } +- $theme_settings['global']['settings']['spacing']['customPadding'] = true; ++ $theme_settings['settings'][ $all_blocks ]['spacing']['customPadding'] = true; + } + ++ // Things that didn't land in core yet, so didn't have a setting assigned. + if ( current( (array) get_theme_support( 'experimental-link-color' ) ) ) { +- if ( ! isset( $theme_settings['global']['settings']['color'] ) ) { +- $theme_settings['global']['settings']['color'] = array(); ++ if ( ! isset( $theme_settings['settings'][ $all_blocks ]['color'] ) ) { ++ $theme_settings['settings'][ $all_blocks ]['color'] = array(); + } +- $theme_settings['global']['settings']['color']['link'] = true; ++ $theme_settings['settings'][ $all_blocks ]['color']['link'] = true; + } + + return $theme_settings; +@@ -196,6 +206,7 @@ function gutenberg_experimental_global_styles_settings( $settings ) { + unset( $settings['disableCustomGradients'] ); + unset( $settings['enableCustomLineHeight'] ); + unset( $settings['enableCustomUnits'] ); ++ unset( $settings['enableCustomSpacing'] ); + unset( $settings['fontSizes'] ); + unset( $settings['gradients'] ); + +diff --git a/lib/load.php b/lib/load.php +index cff4bb2f49..0e5c8dba8f 100644 +--- a/lib/load.php ++++ b/lib/load.php +@@ -40,6 +40,9 @@ if ( class_exists( 'WP_REST_Controller' ) ) { + if ( ! class_exists( 'WP_REST_Widgets_Controller' ) ) { + require_once __DIR__ . '/class-wp-rest-widgets-controller.php'; + } ++ if ( ! class_exists( 'WP_REST_Pattern_Directory_Controller' ) ) { ++ require dirname( __FILE__ ) . '/class-wp-rest-pattern-directory-controller.php'; ++ } + if ( ! class_exists( 'WP_REST_Menus_Controller' ) ) { + require_once __DIR__ . '/class-wp-rest-menus-controller.php'; + } +@@ -105,6 +108,7 @@ require __DIR__ . '/experiments-page.php'; + require __DIR__ . '/class-wp-theme-json.php'; + require __DIR__ . '/class-wp-theme-json-resolver.php'; + require __DIR__ . '/global-styles.php'; ++require __DIR__ . '/query-utils.php'; + + if ( ! class_exists( 'WP_Block_Supports' ) ) { + require_once __DIR__ . '/class-wp-block-supports.php'; +diff --git a/lib/query-utils.php b/lib/query-utils.php +new file mode 100644 +index 0000000000..477772728a +--- /dev/null ++++ b/lib/query-utils.php +@@ -0,0 +1,67 @@ ++ 'post', ++ 'order' => 'DESC', ++ 'orderby' => 'date', ++ 'post__not_in' => array(), ++ ); ++ ++ if ( isset( $block->context['query'] ) ) { ++ if ( isset( $block->context['query']['postType'] ) ) { ++ $query['post_type'] = $block->context['query']['postType']; ++ } ++ if ( isset( $block->context['query']['sticky'] ) && ! empty( $block->context['query']['sticky'] ) ) { ++ $sticky = get_option( 'sticky_posts' ); ++ if ( 'only' === $block->context['query']['sticky'] ) { ++ $query['post__in'] = $sticky; ++ } else { ++ $query['post__not_in'] = array_merge( $query['post__not_in'], $sticky ); ++ } ++ } ++ if ( isset( $block->context['query']['exclude'] ) ) { ++ $query['post__not_in'] = array_merge( $query['post__not_in'], $block->context['query']['exclude'] ); ++ } ++ if ( isset( $block->context['query']['perPage'] ) ) { ++ $query['offset'] = ( $block->context['query']['perPage'] * ( $page - 1 ) ) + $block->context['query']['offset']; ++ $query['posts_per_page'] = $block->context['query']['perPage']; ++ } ++ if ( isset( $block->context['query']['categoryIds'] ) ) { ++ $query['category__in'] = $block->context['query']['categoryIds']; ++ } ++ if ( isset( $block->context['query']['tagIds'] ) ) { ++ $query['tag__in'] = $block->context['query']['tagIds']; ++ } ++ if ( isset( $block->context['query']['order'] ) ) { ++ $query['order'] = strtoupper( $block->context['query']['order'] ); ++ } ++ if ( isset( $block->context['query']['orderBy'] ) ) { ++ $query['orderby'] = $block->context['query']['orderBy']; ++ } ++ if ( isset( $block->context['query']['author'] ) ) { ++ $query['author'] = $block->context['query']['author']; ++ } ++ if ( isset( $block->context['query']['search'] ) ) { ++ $query['s'] = $block->context['query']['search']; ++ } ++ } ++ return $query; ++} +diff --git a/lib/rest-api.php b/lib/rest-api.php +index add3d35f24..dca744c2f4 100644 +--- a/lib/rest-api.php ++++ b/lib/rest-api.php +@@ -10,6 +10,15 @@ if ( ! defined( 'ABSPATH' ) ) { + die( 'Silence is golden.' ); + } + ++/** ++ * Registers the block pattern directory. ++ */ ++function gutenberg_register_rest_pattern_directory() { ++ $block_directory_controller = new WP_REST_Pattern_Directory_Controller(); ++ $block_directory_controller->register_routes(); ++} ++add_filter( 'rest_api_init', 'gutenberg_register_rest_pattern_directory' ); ++ + /** + * Registers the menu locations area REST API routes. + */ +diff --git a/lib/template-canvas.php b/lib/template-canvas.php +index a0e0da7ea0..4d29113dbf 100644 +--- a/lib/template-canvas.php ++++ b/lib/template-canvas.php +@@ -5,6 +5,11 @@ + * @package gutenberg + */ + ++/** ++ * Get the template HTML. ++ * This needs to run before so that blocks can add scripts and styles in wp_head(). ++ */ ++$template_html = gutenberg_get_the_template_html(); + ?> + + > +@@ -16,7 +21,7 @@ + > + + +- ++ + + + diff --git a/5.7-changes.diff b/5.7-changes.diff new file mode 100644 index 00000000000000..4e7a33d430cce7 --- /dev/null +++ b/5.7-changes.diff @@ -0,0 +1,11328 @@ +Skip. Minimum version bump +diff --git a/lib/block-directory.php b/lib/block-directory.php +deleted file mode 100644 +index cb1ec4bd9d..0000000000 +--- a/lib/block-directory.php ++++ /dev/null +@@ -1,54 +0,0 @@ +-` tag for the enqueued script. +- * @param string $handle The script's registered handle. +- * @param string $esc_src The script's pre-escaped registered src. +- * +- * @return string Filtered script tag. +- */ +- function gutenberg_change_script_tag( $tag, $handle, $esc_src ) { +- if ( ! is_admin() ) { +- return $tag; +- } +- +- $tag = str_replace( +- sprintf( "", $esc_src ), +- sprintf( "", esc_attr( $handle ), $esc_src ), +- $tag +- ); +- +- return $tag; +- } +- add_filter( 'script_loader_tag', 'gutenberg_change_script_tag', 1, 3 ); +-} +Skip. Minimum version bump +diff --git a/lib/block-patterns.php b/lib/block-patterns.php +deleted file mode 100644 +index e8c0d0811d..0000000000 +--- a/lib/block-patterns.php ++++ /dev/null +@@ -1,66 +0,0 @@ +-get_all_registered(); +- $settings['__experimentalBlockPatternCategories'] = WP_Block_Pattern_Categories_Registry::get_instance()->get_all_registered(); +- +- return $settings; +-} +-add_filter( 'block_editor_settings', 'gutenberg_extend_settings_block_patterns', 0 ); +- +- +-/** +- * Load a block pattern by name. +- * +- * @param string $name Block Pattern File name. +- * +- * @return array Block Pattern Array. +- */ +-function gutenberg_load_block_pattern( $name ) { +- return require( __DIR__ . '/patterns/' . $name . '.php' ); +-} +- +-/** +- * Register default patterns and categories, potentially overriding ones that were already registered in Core. +- * +- * This can be removed when plugin support requires WordPress 5.5.0+, and patterns have been synced back to Core. +- * +- * @see https://core.trac.wordpress.org/ticket/50550 +- */ +-function gutenberg_register_block_patterns() { +- $should_register_core_patterns = get_theme_support( 'core-block-patterns' ); +- +- if ( $should_register_core_patterns ) { +- register_block_pattern( 'core/text-two-columns', gutenberg_load_block_pattern( 'text-two-columns' ) ); +- register_block_pattern( 'core/two-buttons', gutenberg_load_block_pattern( 'two-buttons' ) ); +- register_block_pattern( 'core/two-images', gutenberg_load_block_pattern( 'two-images' ) ); +- register_block_pattern( 'core/text-two-columns-with-images', gutenberg_load_block_pattern( 'text-two-columns-with-images' ) ); +- register_block_pattern( 'core/text-three-columns-buttons', gutenberg_load_block_pattern( 'text-three-columns-buttons' ) ); +- register_block_pattern( 'core/large-header', gutenberg_load_block_pattern( 'large-header' ) ); +- register_block_pattern( 'core/large-header-button', gutenberg_load_block_pattern( 'large-header-button' ) ); +- register_block_pattern( 'core/three-buttons', gutenberg_load_block_pattern( 'three-buttons' ) ); +- register_block_pattern( 'core/heading-paragraph', gutenberg_load_block_pattern( 'heading-paragraph' ) ); +- register_block_pattern( 'core/quote', gutenberg_load_block_pattern( 'quote' ) ); +- } +- +- register_block_pattern_category( 'buttons', array( 'label' => _x( 'Buttons', 'Block pattern category', 'gutenberg' ) ) ); +- register_block_pattern_category( 'columns', array( 'label' => _x( 'Columns', 'Block pattern category', 'gutenberg' ) ) ); +- register_block_pattern_category( 'gallery', array( 'label' => _x( 'Gallery', 'Block pattern category', 'gutenberg' ) ) ); +- register_block_pattern_category( 'header', array( 'label' => _x( 'Headers', 'Block pattern category', 'gutenberg' ) ) ); +- register_block_pattern_category( 'text', array( 'label' => _x( 'Text', 'Block pattern category', 'gutenberg' ) ) ); +-} +-add_action( 'init', 'gutenberg_register_block_patterns' ); +Skip. Border is still experimental +diff --git a/lib/block-supports/border.php b/lib/block-supports/border.php +new file mode 100644 +index 0000000000..085cdda187 +--- /dev/null ++++ b/lib/block-supports/border.php +@@ -0,0 +1,89 @@ ++attributes ) { ++ $block_type->attributes = array(); ++ } ++ ++ if ( $has_border_radius_support && ! array_key_exists( 'style', $block_type->attributes ) ) { ++ $block_type->attributes['style'] = array( ++ 'type' => 'object', ++ ); ++ } ++} ++ ++/** ++ * Adds CSS classes and inline styles for border styles to the incoming ++ * attributes array. This will be applied to the block markup in the front-end. ++ * ++ * @param WP_Block_type $block_type Block type. ++ * @param array $block_attributes Block attributes. ++ * ++ * @return array Border CSS classes and inline styles. ++ */ ++function gutenberg_apply_border_support( $block_type, $block_attributes ) { ++ // Arrays used to ease addition of further border related features in future. ++ $styles = array(); ++ ++ // Border Radius. ++ if ( gutenberg_has_border_support( $block_type, 'radius' ) ) { ++ if ( isset( $block_attributes['style']['border']['radius'] ) ) { ++ $border_radius = intval( $block_attributes['style']['border']['radius'] ); ++ $styles[] = sprintf( 'border-radius: %dpx;', $border_radius ); ++ } ++ } ++ ++ // Border width, style etc can be added here. ++ ++ // Collect classes and styles. ++ $attributes = array(); ++ ++ if ( ! empty( $styles ) ) { ++ $attributes['style'] = implode( ' ', $styles ); ++ } ++ ++ return $attributes; ++} ++ ++/** ++ * Checks whether the current block type supports the feature requested. ++ * ++ * @param WP_Block_Type $block_type Block type to check for support. ++ * @param string $feature Name of the feature to check support for. ++ * @param mixed $default Fallback value for feature support, defaults to false. ++ * ++ * @return boolean Whether or not the feature is supported. ++ */ ++function gutenberg_has_border_support( $block_type, $feature, $default = false ) { ++ $block_support = false; ++ if ( property_exists( $block_type, 'supports' ) ) { ++ $block_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalBorder' ), $default ); ++ } ++ ++ return true === $block_support || ( is_array( $block_support ) && gutenberg_experimental_get( $block_support, array( $feature ), false ) ); ++} ++ ++// Register the block support. ++WP_Block_Supports::get_instance()->register( ++ 'border', ++ array( ++ 'register_attribute' => 'gutenberg_register_border_support', ++ 'apply' => 'gutenberg_apply_border_support', ++ ) ++); +Skip. Can't remove this due to BC. +diff --git a/lib/block-supports/generated-classname.php b/lib/block-supports/generated-classname.php +index 5f77d3fc90..b43260d513 100644 +--- a/lib/block-supports/generated-classname.php ++++ b/lib/block-supports/generated-classname.php +@@ -35,11 +35,10 @@ function gutenberg_get_block_default_classname( $block_name ) { + * Add the generated classnames to the output. + * + * @param WP_Block_Type $block_type Block Type. +- * @param array $block_attributes Block attributes. + * + * @return array Block CSS classes and inline styles. + */ +-function gutenberg_apply_generated_classname_support( $block_type, $block_attributes ) { ++function gutenberg_apply_generated_classname_support( $block_type ) { + $has_generated_classname_support = true; + $attributes = array(); + if ( property_exists( $block_type, 'supports' ) ) { +Skip. New typography props here are all __experimental. +diff --git a/lib/block-supports/typography.php b/lib/block-supports/typography.php +index f5709e776d..29454c4329 100644 +--- a/lib/block-supports/typography.php ++++ b/lib/block-supports/typography.php +@@ -11,21 +11,29 @@ + * @param WP_Block_Type $block_type Block Type. + */ + function gutenberg_register_typography_support( $block_type ) { +- $has_font_size_support = false; +- if ( property_exists( $block_type, 'supports' ) ) { +- $has_font_size_support = gutenberg_experimental_get( $block_type->supports, array( 'fontSize' ), false ); ++ if ( ! property_exists( $block_type, 'supports' ) ) { ++ return; + } + +- $has_line_height_support = false; +- if ( property_exists( $block_type, 'supports' ) ) { +- $has_line_height_support = gutenberg_experimental_get( $block_type->supports, array( 'lineHeight' ), false ); +- } ++ $has_font_size_support = gutenberg_experimental_get( $block_type->supports, array( 'fontSize' ), false ); ++ $has_font_style_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalFontStyle' ), false ); ++ $has_font_weight_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalFontWeight' ), false ); ++ $has_line_height_support = gutenberg_experimental_get( $block_type->supports, array( 'lineHeight' ), false ); ++ $has_text_decoration_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalTextDecoration' ), false ); ++ $has_text_transform_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalTextTransform' ), false ); ++ ++ $has_typography_support = $has_font_size_support ++ || $has_font_weight_support ++ || $has_font_style_support ++ || $has_line_height_support ++ || $has_text_transform_support ++ || $has_text_decoration_support; + + if ( ! $block_type->attributes ) { + $block_type->attributes = array(); + } + +- if ( ( $has_font_size_support || $has_line_height_support ) && ! array_key_exists( 'style', $block_type->attributes ) ) { ++ if ( $has_typography_support && ! array_key_exists( 'style', $block_type->attributes ) ) { + $block_type->attributes['style'] = array( + 'type' => 'object', + ); +@@ -39,26 +47,30 @@ function gutenberg_register_typography_support( $block_type ) { + } + + /** +- * Add CSS classes and inline styles for font sizes to the incoming attributes array. +- * This will be applied to the block markup in the front-end. ++ * Add CSS classes and inline styles for typography features such as font sizes ++ * to the incoming attributes array. This will be applied to the block markup in ++ * the front-end. + * + * @param WP_Block_Type $block_type Block type. + * @param array $block_attributes Block attributes. + * +- * @return array Font size CSS classes and inline styles. ++ * @return array Typography CSS classes and inline styles. + */ + function gutenberg_apply_typography_support( $block_type, $block_attributes ) { +- $has_font_size_support = false; +- $classes = array(); +- $styles = array(); +- if ( property_exists( $block_type, 'supports' ) ) { +- $has_font_size_support = gutenberg_experimental_get( $block_type->supports, array( 'fontSize' ), false ); ++ if ( ! property_exists( $block_type, 'supports' ) ) { ++ return array(); + } + +- $has_line_height_support = false; +- if ( property_exists( $block_type, 'supports' ) ) { +- $has_line_height_support = gutenberg_experimental_get( $block_type->supports, array( 'lineHeight' ), false ); +- } ++ $classes = array(); ++ $styles = array(); ++ ++ $has_font_family_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalFontFamily' ), false ); ++ $has_font_style_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalFontStyle' ), false ); ++ $has_font_weight_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalFontWeight' ), false ); ++ $has_font_size_support = gutenberg_experimental_get( $block_type->supports, array( 'fontSize' ), false ); ++ $has_line_height_support = gutenberg_experimental_get( $block_type->supports, array( 'lineHeight' ), false ); ++ $has_text_decoration_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalTextDecoration' ), false ); ++ $has_text_transform_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalTextTransform' ), false ); + + // Font Size. + if ( $has_font_size_support ) { +@@ -73,6 +85,41 @@ function gutenberg_apply_typography_support( $block_type, $block_attributes ) { + } + } + ++ // Font Family. ++ if ( $has_font_family_support ) { ++ $has_font_family = isset( $block_attributes['style']['typography']['fontFamily'] ); ++ // Apply required class and style. ++ if ( $has_font_family ) { ++ $font_family = $block_attributes['style']['typography']['fontFamily']; ++ if ( strpos( $font_family, 'var:preset|font-family' ) !== false ) { ++ // Get the name from the string and add proper styles. ++ $index_to_splice = strrpos( $font_family, '|' ) + 1; ++ $font_family_name = substr( $font_family, $index_to_splice ); ++ $styles[] = sprintf( 'font-family: var(--wp--preset--font-family--%s);', $font_family_name ); ++ } else { ++ $styles[] = sprintf( 'font-family: %s;', $block_attributes['style']['color']['fontFamily'] ); ++ } ++ } ++ } ++ ++ // Font style. ++ if ( $has_font_style_support ) { ++ // Apply font style. ++ $font_style = gutenberg_typography_get_css_variable_inline_style( $block_attributes, 'fontStyle', 'font-style' ); ++ if ( $font_style ) { ++ $styles[] = $font_style; ++ } ++ } ++ ++ // Font weight. ++ if ( $has_font_weight_support ) { ++ // Apply font weight. ++ $font_weight = gutenberg_typography_get_css_variable_inline_style( $block_attributes, 'fontWeight', 'font-weight' ); ++ if ( $font_weight ) { ++ $styles[] = $font_weight; ++ } ++ } ++ + // Line Height. + if ( $has_line_height_support ) { + $has_line_height = isset( $block_attributes['style']['typography']['lineHeight'] ); +@@ -82,6 +129,22 @@ function gutenberg_apply_typography_support( $block_type, $block_attributes ) { + } + } + ++ // Text Decoration. ++ if ( $has_text_decoration_support ) { ++ $text_decoration_style = gutenberg_typography_get_css_variable_inline_style( $block_attributes, 'textDecoration', 'text-decoration' ); ++ if ( $text_decoration_style ) { ++ $styles[] = $text_decoration_style; ++ } ++ } ++ ++ // Text Transform. ++ if ( $has_text_transform_support ) { ++ $text_transform_style = gutenberg_typography_get_css_variable_inline_style( $block_attributes, 'textTransform', 'text-transform' ); ++ if ( $text_transform_style ) { ++ $styles[] = $text_transform_style; ++ } ++ } ++ + $attributes = array(); + if ( ! empty( $classes ) ) { + $attributes['class'] = implode( ' ', $classes ); +@@ -93,6 +156,37 @@ function gutenberg_apply_typography_support( $block_type, $block_attributes ) { + return $attributes; + } + ++/** ++ * Generates an inline style for a typography feature e.g. text decoration, ++ * text transform, and font style. ++ * ++ * @param array $attributes Block's attributes. ++ * @param string $feature Key for the feature within the typography styles. ++ * @param string $css_property Slug for the CSS property the inline style sets. ++ * ++ * @return string CSS inline style. ++ */ ++function gutenberg_typography_get_css_variable_inline_style( $attributes, $feature, $css_property ) { ++ // Retrieve current attribute value or skip if not found. ++ $style_value = gutenberg_experimental_get( $attributes, array( 'style', 'typography', $feature ), false ); ++ if ( ! $style_value ) { ++ return; ++ } ++ ++ // If we don't have a preset CSS variable, we'll assume it's a regular CSS value. ++ if ( strpos( $style_value, "var:preset|{$css_property}|" ) === false ) { ++ return sprintf( '%s:%s;', $css_property, $style_value ); ++ } ++ ++ // We have a preset CSS variable as the style. ++ // Get the style value from the string and return CSS style. ++ $index_to_splice = strrpos( $style_value, '|' ) + 1; ++ $slug = substr( $style_value, $index_to_splice ); ++ ++ // Return the actual CSS inline style e.g. `text-decoration:var(--wp--preset--text-decoration--underline);`. ++ return sprintf( '%s:var(--wp--preset--%s--%s);', $css_property, $css_property, $slug ); ++} ++ + // Register the block support. + WP_Block_Supports::get_instance()->register( + 'typography', +Renamed classic to freeform. Skipped rest as Core loads blocks and block styles differently. +diff --git a/lib/blocks.php b/lib/blocks.php +index 4245d4cc14..f4f5a6536c 100644 +--- a/lib/blocks.php ++++ b/lib/blocks.php +@@ -12,12 +12,12 @@ + function gutenberg_reregister_core_block_types() { + // Blocks directory may not exist if working from a fresh clone. + $blocks_dirs = array( +- dirname( __FILE__ ) . '/../build/block-library/blocks/' => array( ++ __DIR__ . '/../build/block-library/blocks/' => array( + 'block_folders' => array( + 'audio', + 'button', + 'buttons', +- 'classic', ++ 'freeform', + 'code', + 'column', + 'columns', +@@ -49,25 +49,20 @@ function gutenberg_reregister_core_block_types() { + ), + 'block_names' => array_merge( + array( +- 'archives.php' => 'core/archives', +- 'block.php' => 'core/block', +- 'calendar.php' => 'core/calendar', +- 'categories.php' => 'core/categories', +- 'cover.php' => 'core/cover', +- 'latest-comments.php' => 'core/latest-comments', +- 'latest-posts.php' => 'core/latest-posts', +- 'navigation.php' => 'core/navigation', +- 'navigation-link.php' => 'core/navigation-link', +- 'rss.php' => 'core/rss', +- 'search.php' => 'core/search', +- 'shortcode.php' => 'core/shortcode', +- 'social-link.php' => 'core/social-link', +- 'tag-cloud.php' => 'core/tag-cloud', +- ), +- ! gutenberg_is_experiment_enabled( 'gutenberg-full-site-editing' ) +- ? array() +- : +- array( ++ 'archives.php' => 'core/archives', ++ 'block.php' => 'core/block', ++ 'calendar.php' => 'core/calendar', ++ 'categories.php' => 'core/categories', ++ 'cover.php' => 'core/cover', ++ 'latest-comments.php' => 'core/latest-comments', ++ 'latest-posts.php' => 'core/latest-posts', ++ 'navigation.php' => 'core/navigation', ++ 'navigation-link.php' => 'core/navigation-link', ++ 'rss.php' => 'core/rss', ++ 'search.php' => 'core/search', ++ 'shortcode.php' => 'core/shortcode', ++ 'social-link.php' => 'core/social-link', ++ 'tag-cloud.php' => 'core/tag-cloud', + 'post-author.php' => 'core/post-author', + 'post-comment.php' => 'core/post-comment', + 'post-comment-author.php' => 'core/post-comment-author', +@@ -93,7 +88,7 @@ function gutenberg_reregister_core_block_types() { + ) + ), + ), +- dirname( __FILE__ ) . '/../build/edit-widgets/blocks/' => array( ++ __DIR__ . '/../build/edit-widgets/blocks/' => array( + 'block_folders' => array( + 'legacy-widget', + 'widget-area', +@@ -105,9 +100,6 @@ function gutenberg_reregister_core_block_types() { + ), + ); + foreach ( $blocks_dirs as $blocks_dir => $details ) { +- if ( ! file_exists( $blocks_dir ) ) { +- return; +- } + $block_folders = $details['block_folders']; + $block_names = $details['block_names']; + +@@ -115,9 +107,6 @@ function gutenberg_reregister_core_block_types() { + + foreach ( $block_folders as $folder_name ) { + $block_json_file = $blocks_dir . $folder_name . '/block.json'; +- if ( ! file_exists( $block_json_file ) ) { +- return; +- } + + // Ideally, all paths to block metadata files should be listed in + // WordPress core. In this place we should rather use filter +@@ -131,6 +120,7 @@ function gutenberg_reregister_core_block_types() { + $registry->unregister( $metadata['name'] ); + } + ++ gutenberg_register_core_block_styles( $folder_name ); + register_block_type_from_metadata( $block_json_file ); + } + +@@ -143,11 +133,13 @@ function gutenberg_reregister_core_block_types() { + if ( $registry->is_registered( $block_names ) ) { + $registry->unregister( $block_names ); + } ++ gutenberg_register_core_block_styles( $block_names ); + } elseif ( is_array( $block_names ) ) { + foreach ( $block_names as $block_name ) { + if ( $registry->is_registered( $block_name ) ) { + $registry->unregister( $block_name ); + } ++ gutenberg_register_core_block_styles( $block_name ); + } + } + +@@ -158,6 +150,46 @@ function gutenberg_reregister_core_block_types() { + + add_action( 'init', 'gutenberg_reregister_core_block_types' ); + ++/** ++ * Registers block styles for a core block. ++ * ++ * @param string $block_name The block-name. ++ * ++ * @return void ++ */ ++function gutenberg_register_core_block_styles( $block_name ) { ++ if ( ! gutenberg_should_load_separate_block_styles() ) { ++ return; ++ } ++ ++ $block_name = str_replace( 'core/', '', $block_name ); ++ ++ $style_path = is_rtl() ++ ? "build/block-library/blocks/$block_name/style-rtl.css" ++ : "build/block-library/blocks/$block_name/style.css"; ++ $editor_style_path = is_rtl() ++ ? "build/block-library/blocks/$block_name/style-editor-rtl.css" ++ : "build/block-library/blocks/$block_name/style-editor.css"; ++ ++ if ( file_exists( gutenberg_dir_path() . $style_path ) ) { ++ wp_register_style( ++ 'wp-block-' . $block_name, ++ gutenberg_url( $style_path ), ++ array(), ++ filemtime( gutenberg_dir_path() . $style_path ) ++ ); ++ } ++ ++ if ( file_exists( gutenberg_dir_path() . $editor_style_path ) ) { ++ wp_register_style( ++ 'wp-block-' . $block_name . '-editor', ++ gutenberg_url( $editor_style_path ), ++ array(), ++ filemtime( gutenberg_dir_path() . $editor_style_path ) ++ ); ++ } ++} ++ + /** + * Complements the implementation of block type `core/social-icon`, whether it + * be provided by core or the plugin, with derived block types for each +Skip. Minimum version bump. +diff --git a/lib/class-wp-block-list.php b/lib/class-wp-block-list.php +deleted file mode 100644 +index 3bf1f62520..0000000000 +--- a/lib/class-wp-block-list.php ++++ /dev/null +@@ -1,194 +0,0 @@ +-blocks = $blocks; +- $this->available_context = $available_context; +- $this->registry = $registry; +- } +- +- /* +- * ArrayAccess interface methods. +- */ +- +- /** +- * Returns true if a block exists by the specified block index, or false +- * otherwise. +- * +- * @link https://www.php.net/manual/en/arrayaccess.offsetexists.php +- * +- * @param string $index Index of block to check. +- * +- * @return bool Whether block exists. +- */ +- public function offsetExists( $index ) { +- return isset( $this->blocks[ $index ] ); +- } +- +- /** +- * Returns the value by the specified block index. +- * +- * @link https://www.php.net/manual/en/arrayaccess.offsetget.php +- * +- * @param string $index Index of block value to retrieve. +- * +- * @return mixed|null Block value if exists, or null. +- */ +- public function offsetGet( $index ) { +- $block = $this->blocks[ $index ]; +- +- if ( isset( $block ) && is_array( $block ) ) { +- $block = new WP_Block( $block, $this->available_context, $this->registry ); +- $this->blocks[ $index ] = $block; +- } +- +- return $block; +- } +- +- /** +- * Assign a block value by the specified block index. +- * +- * @link https://www.php.net/manual/en/arrayaccess.offsetset.php +- * +- * @param string $index Index of block value to set. +- * @param mixed $value Block value. +- */ +- public function offsetSet( $index, $value ) { +- if ( is_null( $index ) ) { +- $this->blocks[] = $value; +- } else { +- $this->blocks[ $index ] = $value; +- } +- } +- +- /** +- * Unset a block. +- * +- * @link https://www.php.net/manual/en/arrayaccess.offsetunset.php +- * +- * @param string $index Index of block value to unset. +- */ +- public function offsetUnset( $index ) { +- unset( $this->blocks[ $index ] ); +- } +- +- /* +- * Iterator interface methods. +- */ +- +- /** +- * Rewinds back to the first element of the Iterator. +- * +- * @link https://www.php.net/manual/en/iterator.rewind.php +- */ +- public function rewind() { +- reset( $this->blocks ); +- } +- +- /** +- * Returns the current element of the block list. +- * +- * @link https://www.php.net/manual/en/iterator.current.php +- * +- * @return mixed Current element. +- */ +- public function current() { +- return $this->offsetGet( $this->key() ); +- } +- +- /** +- * Returns the key of the current element of the block list. +- * +- * @link https://www.php.net/manual/en/iterator.key.php +- * +- * @return mixed Key of the current element. +- */ +- public function key() { +- return key( $this->blocks ); +- } +- +- /** +- * Moves the current position of the block list to the next element. +- * +- * @link https://www.php.net/manual/en/iterator.next.php +- */ +- public function next() { +- next( $this->blocks ); +- } +- +- /** +- * Checks if current position is valid. +- * +- * @link https://www.php.net/manual/en/iterator.valid.php +- */ +- public function valid() { +- return null !== key( $this->blocks ); +- } +- +- /* +- * Countable interface methods. +- */ +- +- /** +- * Returns the count of blocks in the list. +- * +- * @link https://www.php.net/manual/en/countable.count.php +- * +- * @return int Block count. +- */ +- public function count() { +- return count( $this->blocks ); +- } +- +-} +Skip. Minimum version bump +diff --git a/lib/class-wp-block-pattern-categories-registry.php b/lib/class-wp-block-pattern-categories-registry.php +deleted file mode 100644 +index 2404f6a88d..0000000000 +--- a/lib/class-wp-block-pattern-categories-registry.php ++++ /dev/null +@@ -1,144 +0,0 @@ +-registered_categories[ $category_name ] = array_merge( +- array( 'name' => $category_name ), +- $category_properties +- ); +- +- return true; +- } +- +- /** +- * Unregisters a pattern category. +- * +- * @param string $category_name Pattern name including namespace. +- * @return boolean True if the pattern was unregistered with success and false otherwise. +- */ +- public function unregister( $category_name ) { +- if ( ! $this->is_registered( $category_name ) ) { +- /* translators: 1: Block pattern name. */ +- $message = sprintf( __( 'Block pattern category "%1$s" not found.', 'gutenberg' ), $category_name ); +- _doing_it_wrong( __METHOD__, $message, '8.1.0' ); +- return false; +- } +- +- unset( $this->registered_categories[ $category_name ] ); +- +- return true; +- } +- +- /** +- * Retrieves an array containing the properties of a registered pattern category. +- * +- * @param string $category_name Pattern category name. +- * @return array Registered pattern properties. +- */ +- public function get_registered( $category_name ) { +- if ( ! $this->is_registered( $category_name ) ) { +- return null; +- } +- +- return $this->registered_categories[ $category_name ]; +- } +- +- /** +- * Retrieves all registered pattern categories. +- * +- * @return array Array of arrays containing the registered pattern categories properties. +- */ +- public function get_all_registered() { +- return array_values( $this->registered_categories ); +- } +- +- /** +- * Checks if a pattern category is registered. +- * +- * @param string $category_name Pattern category name. +- * @return bool True if the pattern category is registered, false otherwise. +- */ +- public function is_registered( $category_name ) { +- return isset( $this->registered_categories[ $category_name ] ); +- } +- +- /** +- * Utility method to retrieve the main instance of the class. +- * +- * The instance will be created if it does not exist yet. +- * +- * @since 5.3.0 +- * +- * @return WP_Block_Pattern_Categories_Registry The main instance. +- */ +- public static function get_instance() { +- if ( null === self::$instance ) { +- self::$instance = new self(); +- } +- +- return self::$instance; +- } +-} +- +-/** +- * Registers a new pattern category. +- * +- * @param string $category_name Pattern category name. +- * @param array $category_properties Array containing the properties of the category. +- * +- * @return boolean True if the pattern category was registered with success and false otherwise. +- */ +-function register_block_pattern_category( $category_name, $category_properties ) { +- return WP_Block_Pattern_Categories_Registry::get_instance()->register( $category_name, $category_properties ); +-} +- +-/** +- * Unregisters a pattern category. +- * +- * @param string $category_name Pattern category name including namespace. +- * +- * @return boolean True if the pattern category was unregistered with success and false otherwise. +- */ +-function unregister_block_pattern_category( $category_name ) { +- return WP_Block_Pattern_Categories_Registry::get_instance()->unregister( $category_name ); +-} +Skip. Minimum version bump +diff --git a/lib/class-wp-block-patterns-registry.php b/lib/class-wp-block-patterns-registry.php +deleted file mode 100644 +index 56a36bce38..0000000000 +--- a/lib/class-wp-block-patterns-registry.php ++++ /dev/null +@@ -1,157 +0,0 @@ +-registered_patterns[ $pattern_name ] = array_merge( +- $pattern_properties, +- array( 'name' => $pattern_name ) +- ); +- +- return true; +- } +- +- /** +- * Unregisters a pattern. +- * +- * @param string $pattern_name Pattern name including namespace. +- * @return boolean True if the pattern was unregistered with success and false otherwise. +- */ +- public function unregister( $pattern_name ) { +- if ( ! $this->is_registered( $pattern_name ) ) { +- /* translators: 1: Pattern name. */ +- $message = sprintf( __( 'Block pattern "%1$s" not found.', 'gutenberg' ), $pattern_name ); +- _doing_it_wrong( __METHOD__, $message, '7.8.0' ); +- return false; +- } +- +- unset( $this->registered_patterns[ $pattern_name ] ); +- +- return true; +- } +- +- /** +- * Retrieves an array containing the properties of a registered pattern. +- * +- * @param string $pattern_name Pattern name including namespace. +- * @return array Registered pattern properties. +- */ +- public function get_registered( $pattern_name ) { +- if ( ! $this->is_registered( $pattern_name ) ) { +- return null; +- } +- +- return $this->registered_patterns[ $pattern_name ]; +- } +- +- /** +- * Retrieves all registered patterns. +- * +- * @return array Array of arrays containing the registered patterns properties, +- * and per style. +- */ +- public function get_all_registered() { +- return array_values( $this->registered_patterns ); +- } +- +- /** +- * Checks if a pattern is registered. +- * +- * @param string $pattern_name Pattern name including namespace. +- * @return bool True if the pattern is registered, false otherwise. +- */ +- public function is_registered( $pattern_name ) { +- return isset( $this->registered_patterns[ $pattern_name ] ); +- } +- +- /** +- * Utility method to retrieve the main instance of the class. +- * +- * The instance will be created if it does not exist yet. +- * +- * @since 5.3.0 +- * +- * @return WP_Block_Patterns_Registry The main instance. +- */ +- public static function get_instance() { +- if ( null === self::$instance ) { +- self::$instance = new self(); +- } +- +- return self::$instance; +- } +-} +- +-/** +- * Registers a new pattern. +- * +- * @param string $pattern_name Pattern name including namespace. +- * @param array $pattern_properties Array containing the properties of the pattern. +- * +- * @return boolean True if the pattern was registered with success and false otherwise. +- */ +-function register_block_pattern( $pattern_name, $pattern_properties ) { +- return WP_Block_Patterns_Registry::get_instance()->register( $pattern_name, $pattern_properties ); +-} +- +-/** +- * Unregisters a pattern. +- * +- * @param string $pattern_name Pattern name including namespace. +- * +- * @return boolean True if the pattern was unregistered with success and false otherwise. +- */ +-function unregister_block_pattern( $pattern_name ) { +- return WP_Block_Patterns_Registry::get_instance()->unregister( $pattern_name ); +-} +Removed unnecessary $name. Skipped render_callback check as Core does it differnetly. +diff --git a/lib/class-wp-block-supports.php b/lib/class-wp-block-supports.php +index 45b6bd0189..c3a9342709 100644 +--- a/lib/class-wp-block-supports.php ++++ b/lib/class-wp-block-supports.php +@@ -88,7 +88,7 @@ class WP_Block_Supports { + } + + $output = array(); +- foreach ( $this->block_supports as $name => $block_support_config ) { ++ foreach ( $this->block_supports as $block_support_config ) { + if ( ! isset( $block_support_config['apply'] ) ) { + continue; + } +@@ -127,7 +127,7 @@ class WP_Block_Supports { + $block_type->attributes = array(); + } + +- foreach ( $this->block_supports as $name => $block_support_config ) { ++ foreach ( $this->block_supports as $block_support_config ) { + if ( ! isset( $block_support_config['register_attribute'] ) ) { + continue; + } +@@ -207,9 +207,20 @@ function get_block_wrapper_attributes( $extra_attributes = array() ) { + * @return array Block attributes. + */ + function wp_block_supports_track_block_to_render( $args ) { +- if ( null !== $args['render_callback'] ) { ++ if ( is_callable( $args['render_callback'] ) ) { + $block_render_callback = $args['render_callback']; +- $args['render_callback'] = function( $attributes, $content, $block ) use ( $block_render_callback ) { ++ $args['render_callback'] = function( $attributes, $content, $block = null ) use ( $block_render_callback ) { ++ // Check for null for back compatibility with WP_Block_Type->render ++ // which is unused since the introduction of WP_Block class. ++ // ++ // See: ++ // - https://core.trac.wordpress.org/ticket/49927 ++ // - commit 910de8f6890c87f93359c6f2edc6c27b9a3f3292 at wordpress-develop. ++ ++ if ( null === $block ) { ++ return $block_render_callback( $attributes, $content ); ++ } ++ + $parent_block = WP_Block_Supports::$block_to_render; + WP_Block_Supports::$block_to_render = $block->parsed_block; + $result = $block_render_callback( $attributes, $content, $block ); +Skip. Minimum version bump +diff --git a/lib/class-wp-block.php b/lib/class-wp-block.php +deleted file mode 100644 +index cd02905074..0000000000 +--- a/lib/class-wp-block.php ++++ /dev/null +@@ -1,243 +0,0 @@ +- testing..." -> "Just testing..." +- * +- * @var string +- */ +- public $inner_html = ''; +- +- /** +- * List of string fragments and null markers where inner blocks were found +- * +- * @example array( +- * 'inner_html' => 'BeforeInnerAfter', +- * 'inner_blocks' => array( block, block ), +- * 'inner_content' => array( 'Before', null, 'Inner', null, 'After' ), +- * ) +- * +- * @var array +- */ +- public $inner_content = array(); +- +- /** +- * Constructor. +- * +- * Populates object properties from the provided block instance argument. +- * +- * The given array of context values will not necessarily be available on +- * the instance itself, but is treated as the full set of values provided by +- * the block's ancestry. This is assigned to the private `available_context` +- * property. Only values which are configured to consumed by the block via +- * its registered type will be assigned to the block's `context` property. +- * +- * @param array $block Array of parsed block properties. +- * @param array $available_context Optional array of ancestry context values. +- * @param WP_Block_Type_Registry $registry Optional block type registry. +- */ +- public function __construct( $block, $available_context = array(), $registry = null ) { +- $this->parsed_block = $block; +- $this->name = $block['blockName']; +- +- if ( is_null( $registry ) ) { +- $registry = WP_Block_Type_Registry::get_instance(); +- } +- +- $this->block_type = $registry->get_registered( $this->name ); +- +- if ( ! empty( $this->block_type->context ) ) { +- $message = sprintf( +- /* translators: 1: Block name. */ +- __( 'The "context" parameter provided in block type "%s" is deprecated. Please use "uses_context" instead.', 'gutenberg' ), +- $this->name +- ); +- _doing_it_wrong( __CLASS__, $message, '8.6.0' ); +- $this->block_type->uses_context = $this->block_type->context; +- } +- if ( ! empty( $this->block_type->providesContext ) ) { +- $message = sprintf( +- /* translators: 1: Block name. */ +- __( 'The "providesContext" parameter provided in block type "%s" is deprecated. Please use "provides_context".', 'gutenberg' ), +- $this->name +- ); +- _doing_it_wrong( __CLASS__, $message, '8.6.0' ); +- $this->block_type->provides_context = $this->block_type->providesContext; +- } +- +- $this->available_context = $available_context; +- +- if ( ! empty( $this->block_type->uses_context ) ) { +- foreach ( $this->block_type->uses_context as $context_name ) { +- if ( array_key_exists( $context_name, $this->available_context ) ) { +- $this->context[ $context_name ] = $this->available_context[ $context_name ]; +- } +- } +- } +- +- if ( ! empty( $block['innerBlocks'] ) ) { +- $child_context = $this->available_context; +- +- if ( ! empty( $this->block_type->provides_context ) ) { +- foreach ( $this->block_type->provides_context as $context_name => $attribute_name ) { +- if ( array_key_exists( $attribute_name, $this->attributes ) ) { +- $child_context[ $context_name ] = $this->attributes[ $attribute_name ]; +- } +- } +- } +- +- $this->inner_blocks = new WP_Block_List( $block['innerBlocks'], $child_context, $registry ); +- } +- +- if ( ! empty( $block['innerHTML'] ) ) { +- $this->inner_html = $block['innerHTML']; +- } +- +- if ( ! empty( $block['innerContent'] ) ) { +- $this->inner_content = $block['innerContent']; +- } +- } +- +- /** +- * Returns a value from an inaccessible property. +- * +- * This is used to lazily initialize the `attributes` property of a block, +- * such that it is only prepared with default attributes at the time that +- * the property is accessed. For all other inaccessible properties, a `null` +- * value is returned. +- * +- * @param string $name Property name. +- * +- * @return array|null Prepared attributes, or null. +- */ +- public function __get( $name ) { +- if ( 'attributes' === $name ) { +- $this->attributes = isset( $this->parsed_block['attrs'] ) ? +- $this->parsed_block['attrs'] : +- array(); +- +- if ( ! is_null( $this->block_type ) ) { +- $this->attributes = $this->block_type->prepare_attributes_for_render( $this->attributes ); +- } +- +- return $this->attributes; +- } +- +- return null; +- } +- +- /** +- * Generates the render output for the block. +- * +- * @param array $options { +- * Optional options object. +- * +- * @type bool $dynamic Defaults to 'true'. Optionally set to false to avoid using the block's render_callback. +- * } +- * +- * @return string Rendered block output. +- */ +- public function render( $options = array() ) { +- global $post; +- $options = array_replace( +- array( +- 'dynamic' => true, +- ), +- $options +- ); +- +- $is_dynamic = $options['dynamic'] && $this->name && null !== $this->block_type && $this->block_type->is_dynamic(); +- $block_content = ''; +- +- if ( ! $options['dynamic'] || empty( $this->block_type->skip_inner_blocks ) ) { +- $index = 0; +- foreach ( $this->inner_content as $chunk ) { +- $block_content .= is_string( $chunk ) ? +- $chunk : +- $this->inner_blocks[ $index++ ]->render(); +- } +- } +- +- if ( $is_dynamic ) { +- $global_post = $post; +- $block_content = (string) call_user_func( $this->block_type->render_callback, $this->attributes, $block_content, $this ); +- $post = $global_post; +- } +- +- if ( ! empty( $this->block_type->script ) ) { +- wp_enqueue_script( $this->block_type->script ); +- } +- +- if ( ! empty( $this->block_type->style ) ) { +- wp_enqueue_style( $this->block_type->style ); +- } +- +- /** This filter is documented in src/wp-includes/blocks.php */ +- return apply_filters( 'render_block', $block_content, $this->parsed_block ); +- } +- +-} +Skip. Batch endpoint is still experimental +diff --git a/lib/class-wp-rest-batch-controller.php b/lib/class-wp-rest-batch-controller.php +index 514d9332de..86a6f11bd6 100644 +--- a/lib/class-wp-rest-batch-controller.php ++++ b/lib/class-wp-rest-batch-controller.php +@@ -110,7 +110,7 @@ class WP_REST_Batch_Controller { + $requests[] = $single_request; + } + +- if ( ! method_exists( rest_get_server(), 'match_request_to_handler' ) ) { ++ if ( ! is_callable( array( rest_get_server(), 'match_request_to_handler' ) ) ) { + return $this->polyfill_batching( $requests ); + } + +Skip. Minimum version bump +diff --git a/lib/class-wp-rest-block-directory-controller.php b/lib/class-wp-rest-block-directory-controller.php +deleted file mode 100644 +index df1f2e4085..0000000000 +--- a/lib/class-wp-rest-block-directory-controller.php ++++ /dev/null +@@ -1,343 +0,0 @@ +-namespace = 'wp/v2'; +- $this->rest_base = 'block-directory'; +- } +- +- /** +- * Registers the necessary REST API routes. +- */ +- public function register_routes() { +- register_rest_route( +- $this->namespace, +- '/' . $this->rest_base . '/search', +- array( +- array( +- 'methods' => WP_REST_Server::READABLE, +- 'callback' => array( $this, 'get_items' ), +- 'permission_callback' => array( $this, 'get_items_permissions_check' ), +- 'args' => $this->get_collection_params(), +- ), +- 'schema' => array( $this, 'get_public_item_schema' ), +- ) +- ); +- } +- +- /** +- * Checks whether a given request has permission to install and activate plugins. +- * +- * @since 5.5.0 +- * +- * @param WP_REST_Request $request Full details about the request. +- * +- * @return WP_Error|bool True if the request has permission, WP_Error object otherwise. +- */ +- public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable +- if ( ! current_user_can( 'install_plugins' ) || ! current_user_can( 'activate_plugins' ) ) { +- return new WP_Error( +- 'rest_block_directory_cannot_view', +- __( 'Sorry, you are not allowed to browse the block directory.', 'gutenberg' ), +- array( 'status' => rest_authorization_required_code() ) +- ); +- } +- +- return true; +- } +- +- /** +- * Search and retrieve blocks metadata +- * +- * @since 5.5.0 +- * +- * @param WP_REST_Request $request Full details about the request. +- * +- * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure. +- */ +- public function get_items( $request ) { +- require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; +- require_once ABSPATH . 'wp-admin/includes/plugin.php'; +- +- $response = plugins_api( +- 'query_plugins', +- array( +- 'block' => $request['term'], +- 'per_page' => $request['per_page'], +- 'page' => $request['page'], +- ) +- ); +- +- if ( is_wp_error( $response ) ) { +- $response->add_data( array( 'status' => 500 ) ); +- +- return $response; +- } +- +- $result = array(); +- +- foreach ( $response->plugins as $plugin ) { +- $data = $this->prepare_item_for_response( $plugin, $request ); +- $result[] = $this->prepare_response_for_collection( $data ); +- } +- +- return rest_ensure_response( $result ); +- } +- +- /** +- * Parse block metadata for a block, and prepare it for an API repsonse. +- * +- * @since 5.5.0 +- * +- * @param array $plugin The plugin metadata. +- * @param WP_REST_Request $request Request object. +- * +- * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure. +- */ +- public function prepare_item_for_response( $plugin, $request ) { +- // There might be multiple blocks in a plugin. Only the first block is mapped. +- $block_data = reset( $plugin['blocks'] ); +- +- // A data array containing the properties we'll return. +- $block = array( +- 'name' => $block_data['name'], +- 'title' => ( $block_data['title'] ? $block_data['title'] : $plugin['name'] ), +- 'description' => wp_trim_words( $plugin['description'], 30, '...' ), +- 'id' => $plugin['slug'], +- 'rating' => $plugin['rating'] / 20, +- 'rating_count' => intval( $plugin['num_ratings'] ), +- 'active_installs' => intval( $plugin['active_installs'] ), +- 'author_block_rating' => $plugin['author_block_rating'] / 20, +- 'author_block_count' => intval( $plugin['author_block_count'] ), +- 'author' => wp_strip_all_tags( $plugin['author'] ), +- 'icon' => ( isset( $plugin['icons']['1x'] ) ? $plugin['icons']['1x'] : 'block-default' ), +- 'assets' => array(), +- 'last_updated' => $plugin['last_updated'], +- 'humanized_updated' => sprintf( +- /* translators: %s: Human-readable time difference. */ +- __( '%s ago', 'gutenberg' ), +- human_time_diff( strtotime( $plugin['last_updated'] ) ) +- ), +- ); +- +- foreach ( $plugin['block_assets'] as $asset ) { +- // Allow for fully qualified URLs in future. +- if ( 'https' === wp_parse_url( $asset, PHP_URL_SCHEME ) && ! empty( wp_parse_url( $asset, PHP_URL_HOST ) ) ) { +- $block['assets'][] = esc_url_raw( +- $asset, +- array( 'https' ) +- ); +- } else { +- $block['assets'][] = esc_url_raw( +- add_query_arg( 'v', strtotime( $block['last_updated'] ), 'https://ps.w.org/' . $plugin['slug'] . $asset ), +- array( 'https' ) +- ); +- } +- } +- +- $this->add_additional_fields_to_object( $block, $request ); +- +- $response = new WP_REST_Response( $block ); +- $response->add_links( $this->prepare_links( $plugin ) ); +- +- return $response; +- } +- +- /** +- * Generates a list of links to include in the response for the plugin. +- * +- * @since 5.5.0 +- * +- * @param array $plugin The plugin data from WordPress.org. +- * +- * @return array +- */ +- protected function prepare_links( $plugin ) { +- $links = array( +- 'https://api.w.org/install-plugin' => array( +- 'href' => add_query_arg( 'slug', urlencode( $plugin['slug'] ), rest_url( 'wp/v2/plugins' ) ), +- ), +- ); +- +- $plugin_file = $this->find_plugin_for_slug( $plugin['slug'] ); +- +- if ( $plugin_file ) { +- $links['https://api.w.org/plugin'] = array( +- 'href' => rest_url( 'wp/v2/plugins/' . substr( $plugin_file, 0, - 4 ) ), +- 'embeddable' => true, +- ); +- } +- +- return $links; +- } +- +- /** +- * Finds an installed plugin for the given slug. +- * +- * @since 5.5.0 +- * +- * @param string $slug The WordPress.org directory slug for a plugin. +- * +- * @return string The plugin file found matching it. +- */ +- protected function find_plugin_for_slug( $slug ) { +- require_once ABSPATH . 'wp-admin/includes/plugin.php'; +- +- $plugin_files = get_plugins( '/' . $slug ); +- +- if ( ! $plugin_files ) { +- return ''; +- } +- +- $plugin_files = array_keys( $plugin_files ); +- +- return $slug . '/' . reset( $plugin_files ); +- } +- +- /** +- * Retrieves the theme's schema, conforming to JSON Schema. +- * +- * @since 5.5.0 +- * +- * @return array Item schema data. +- */ +- public function get_item_schema() { +- if ( $this->schema ) { +- return $this->add_additional_fields_schema( $this->schema ); +- } +- +- $this->schema = array( +- '$schema' => 'http://json-schema.org/draft-04/schema#', +- 'title' => 'block-directory-item', +- 'type' => 'object', +- 'properties' => array( +- 'name' => array( +- 'description' => __( 'The block name, in namespace/block-name format.', 'gutenberg' ), +- 'type' => 'string', +- 'context' => array( 'view' ), +- ), +- 'title' => array( +- 'description' => __( 'The block title, in human readable format.', 'gutenberg' ), +- 'type' => 'string', +- 'context' => array( 'view' ), +- ), +- 'description' => array( +- 'description' => __( 'A short description of the block, in human readable format.', 'gutenberg' ), +- 'type' => 'string', +- 'context' => array( 'view' ), +- ), +- 'id' => array( +- 'description' => __( 'The block slug.', 'gutenberg' ), +- 'type' => 'string', +- 'context' => array( 'view' ), +- ), +- 'rating' => array( +- 'description' => __( 'The star rating of the block.', 'gutenberg' ), +- 'type' => 'integer', +- 'context' => array( 'view' ), +- ), +- 'rating_count' => array( +- 'description' => __( 'The number of ratings.', 'gutenberg' ), +- 'type' => 'integer', +- 'context' => array( 'view' ), +- ), +- 'active_installs' => array( +- 'description' => __( 'The number sites that have activated this block.', 'gutenberg' ), +- 'type' => 'string', +- 'context' => array( 'view' ), +- ), +- 'author_block_rating' => array( +- 'description' => __( 'The average rating of blocks published by the same author.', 'gutenberg' ), +- 'type' => 'integer', +- 'context' => array( 'view' ), +- ), +- 'author_block_count' => array( +- 'description' => __( 'The number of blocks published by the same author.', 'gutenberg' ), +- 'type' => 'integer', +- 'context' => array( 'view' ), +- ), +- 'author' => array( +- 'description' => __( 'The WordPress.org username of the block author.', 'gutenberg' ), +- 'type' => 'string', +- 'context' => array( 'view' ), +- ), +- 'icon' => array( +- 'description' => __( 'The block icon.', 'gutenberg' ), +- 'type' => 'string', +- 'format' => 'uri', +- 'context' => array( 'view' ), +- ), +- 'humanized_updated' => array( +- 'description' => __( 'The date when the block was last updated, in fuzzy human readable format.', 'gutenberg' ), +- 'type' => 'string', +- 'context' => array( 'view' ), +- ), +- 'assets' => array( +- 'description' => __( 'An object representing the block CSS and JavaScript assets.', 'gutenberg' ), +- 'type' => 'array', +- 'context' => array( 'view' ), +- 'readonly' => true, +- 'items' => array( +- 'type' => 'string', +- 'format' => 'uri', +- ), +- +- ), +- +- ), +- ); +- +- return $this->add_additional_fields_schema( $this->schema ); +- } +- +- /** +- * Retrieves the search params for the blocks collection. +- * +- * @since 5.5.0 +- * +- * @return array Collection parameters. +- */ +- public function get_collection_params() { +- $query_params = parent::get_collection_params(); +- +- $query_params['context']['default'] = 'view'; +- +- $query_params['term'] = array( +- 'description' => __( 'Limit result set to blocks matching the search term.', 'gutenberg' ), +- 'type' => 'string', +- 'required' => true, +- 'minLength' => 1, +- ); +- +- unset( $query_params['search'] ); +- +- /** +- * Filter collection parameters for the block directory controller. +- * +- * @since 5.5.0 +- * +- * @param array $query_params JSON Schema-formatted collection parameters. +- */ +- return apply_filters( 'rest_block_directory_collection_params', $query_params ); +- } +-} +Skip. Minimum version bump +diff --git a/lib/class-wp-rest-block-types-controller.php b/lib/class-wp-rest-block-types-controller.php +deleted file mode 100644 +index 32b8a17cc4..0000000000 +--- a/lib/class-wp-rest-block-types-controller.php ++++ /dev/null +@@ -1,528 +0,0 @@ +-namespace = '__experimental'; +- $this->rest_base = 'block-types'; +- $this->block_registry = WP_Block_Type_Registry::get_instance(); +- $this->style_registry = WP_Block_Styles_Registry::get_instance(); +- } +- +- /** +- * Registers the routes for the objects of the controller. +- * +- * @see register_rest_route() +- */ +- public function register_routes() { +- +- register_rest_route( +- $this->namespace, +- '/' . $this->rest_base, +- array( +- array( +- 'methods' => WP_REST_Server::READABLE, +- 'callback' => array( $this, 'get_items' ), +- 'permission_callback' => array( $this, 'get_items_permissions_check' ), +- 'args' => $this->get_collection_params(), +- ), +- 'schema' => array( $this, 'get_public_item_schema' ), +- ) +- ); +- +- register_rest_route( +- $this->namespace, +- '/' . $this->rest_base . '/(?P[a-zA-Z0-9_-]+)', +- array( +- array( +- 'methods' => WP_REST_Server::READABLE, +- 'callback' => array( $this, 'get_items' ), +- 'permission_callback' => array( $this, 'get_items_permissions_check' ), +- 'args' => $this->get_collection_params(), +- ), +- 'schema' => array( $this, 'get_public_item_schema' ), +- ) +- ); +- +- register_rest_route( +- $this->namespace, +- '/' . $this->rest_base . '/(?P[a-zA-Z0-9_-]+)/(?P[a-zA-Z0-9_-]+)', +- array( +- 'args' => array( +- 'name' => array( +- 'description' => __( 'Block name.', 'gutenberg' ), +- 'type' => 'string', +- ), +- 'namespace' => array( +- 'description' => __( 'Block namespace.', 'gutenberg' ), +- 'type' => 'string', +- ), +- ), +- array( +- 'methods' => WP_REST_Server::READABLE, +- 'callback' => array( $this, 'get_item' ), +- 'permission_callback' => array( $this, 'get_item_permissions_check' ), +- 'args' => array( +- 'context' => $this->get_context_param( array( 'default' => 'view' ) ), +- ), +- ), +- 'schema' => array( $this, 'get_public_item_schema' ), +- ) +- ); +- } +- +- /** +- * Checks whether a given request has permission to read post block types. +- * +- * @param WP_REST_Request $request Full details about the request. +- * +- * @return WP_Error|bool True if the request has read access, WP_Error object otherwise. +- */ +- public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable +- return $this->check_read_permission(); +- } +- +- /** +- * Retrieves all post block types, depending on user context. +- * +- * @param WP_REST_Request $request Full details about the request. +- * +- * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure. +- */ +- public function get_items( $request ) { +- $data = array(); +- $block_types = $this->block_registry->get_all_registered(); +- +- // Retrieve the list of registered collection query parameters. +- $registered = $this->get_collection_params(); +- $namespace = ''; +- if ( isset( $registered['namespace'] ) && ! empty( $request['namespace'] ) ) { +- $namespace = $request['namespace']; +- } +- +- foreach ( $block_types as $slug => $obj ) { +- if ( $namespace ) { +- $pieces = explode( '/', $obj->name ); +- $block_namespace = $pieces[0]; +- if ( $namespace !== $block_namespace ) { +- continue; +- } +- } +- $block_type = $this->prepare_item_for_response( $obj, $request ); +- $data[] = $this->prepare_response_for_collection( $block_type ); +- } +- +- return rest_ensure_response( $data ); +- } +- +- /** +- * Checks if a given request has access to read a block type. +- * +- * @param WP_REST_Request $request Full details about the request. +- * +- * @return WP_Error|bool True if the request has read access for the item, WP_Error object otherwise. +- */ +- public function get_item_permissions_check( $request ) { +- $check = $this->check_read_permission(); +- if ( is_wp_error( $check ) ) { +- return $check; +- } +- $block_name = sprintf( '%s/%s', $request['namespace'], $request['name'] ); +- $block_type = $this->get_block( $block_name ); +- if ( is_wp_error( $block_type ) ) { +- return $block_type; +- } +- +- return true; +- } +- +- /** +- * Checks whether a given block type should be visible. +- * +- * @return WP_Error|bool True if the block type is visible, otherwise false. +- */ +- protected function check_read_permission() { +- if ( current_user_can( 'edit_posts' ) ) { +- return true; +- } +- foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) { +- if ( current_user_can( $post_type->cap->edit_posts ) ) { +- return true; +- } +- } +- +- return new WP_Error( 'rest_block_type_cannot_view', __( 'Sorry, you are not allowed to manage block types.', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) ); +- } +- +- /** +- * Get the block, if the name is valid. +- * +- * @param string $name Block name. +- * @return WP_Block_Type|WP_Error Block type object if name is valid, WP_Error otherwise. +- */ +- protected function get_block( $name ) { +- $block_type = $this->block_registry->get_registered( $name ); +- if ( empty( $block_type ) ) { +- return new WP_Error( 'rest_block_type_invalid', __( 'Invalid block type.', 'gutenberg' ), array( 'status' => 404 ) ); +- } +- +- return $block_type; +- } +- +- /** +- * Retrieves a specific block type. +- * +- * @param WP_REST_Request $request Full details about the request. +- * +- * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure. +- */ +- public function get_item( $request ) { +- $block_name = sprintf( '%s/%s', $request['namespace'], $request['name'] ); +- $block_type = $this->get_block( $block_name ); +- if ( is_wp_error( $block_type ) ) { +- return $block_type; +- } +- $data = $this->prepare_item_for_response( $block_type, $request ); +- +- return rest_ensure_response( $data ); +- } +- +- /** +- * Prepares a block type object for serialization. +- * +- * @param WP_Block_Type $block_type block type data. +- * @param WP_REST_Request $request Full details about the request. +- * +- * @return WP_REST_Response block type data. +- */ +- public function prepare_item_for_response( $block_type, $request ) { +- +- $fields = $this->get_fields_for_response( $request ); +- $data = array(); +- +- if ( rest_is_field_included( 'attributes', $fields ) ) { +- $data['attributes'] = $block_type->get_attributes(); +- } +- +- if ( rest_is_field_included( 'is_dynamic', $fields ) ) { +- $data['is_dynamic'] = $block_type->is_dynamic(); +- } +- +- $schema = $this->get_item_schema(); +- $extra_fields = array( +- 'name' => 'name', +- 'title' => 'title', +- 'description' => 'description', +- 'icon' => 'icon', +- 'category' => 'category', +- 'keywords' => 'keywords', +- 'parent' => 'parent', +- 'provides_context' => 'provides_context', +- 'uses_context' => 'uses_context', +- 'supports' => 'supports', +- 'styles' => 'styles', +- 'textdomain' => 'textdomain', +- 'example' => 'example', +- 'editor_script' => 'editor_script', +- 'script' => 'script', +- 'editor_style' => 'editor_style', +- 'style' => 'style', +- ); +- foreach ( $extra_fields as $key => $extra_field ) { +- if ( rest_is_field_included( $key, $fields ) ) { +- if ( isset( $block_type->$extra_field ) ) { +- $field = $block_type->$extra_field; +- } elseif ( array_key_exists( 'default', $schema['properties'][ $key ] ) ) { +- $field = $schema['properties'][ $key ]['default']; +- } else { +- $field = ''; +- } +- $data[ $key ] = rest_sanitize_value_from_schema( $field, $schema['properties'][ $key ] ); +- } +- } +- +- if ( rest_is_field_included( 'styles', $fields ) ) { +- $styles = $this->style_registry->get_registered_styles_for_block( $block_type->name ); +- $styles = array_values( $styles ); +- $data['styles'] = wp_parse_args( $styles, $data['styles'] ); +- } +- +- $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; +- $data = $this->add_additional_fields_to_object( $data, $request ); +- $data = $this->filter_response_by_context( $data, $context ); +- +- $response = rest_ensure_response( $data ); +- +- $response->add_links( $this->prepare_links( $block_type ) ); +- +- /** +- * Filters a block type returned from the REST API. +- * +- * Allows modification of the block type data right before it is returned. +- * +- * @param WP_REST_Response $response The response object. +- * @param object $block_type The original block type object. +- * @param WP_REST_Request $request Request used to generate the response. +- */ +- return apply_filters( 'rest_prepare_block_type', $response, $block_type, $request ); +- } +- +- /** +- * Prepares links for the request. +- * +- * @param WP_Block_Type $block_type block type data. +- * @return array Links for the given block type. +- */ +- protected function prepare_links( $block_type ) { +- $pieces = explode( '/', $block_type->name ); +- $namespace = $pieces[0]; +- $links = array( +- 'collection' => array( +- 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), +- ), +- 'self' => array( +- 'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $block_type->name ) ), +- ), +- 'up' => array( +- 'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $namespace ) ), +- ), +- ); +- +- if ( $block_type->is_dynamic() ) { +- $links['https://api.w.org/render-block']['href'] = add_query_arg( 'context', 'edit', rest_url( sprintf( '%s/%s/%s', 'wp/v2', 'block-renderer', $block_type->name ) ) ); +- } +- +- return $links; +- } +- +- /** +- * Retrieves the block type' schema, conforming to JSON Schema. +- * +- * @return array Item schema data. +- */ +- public function get_item_schema() { +- if ( $this->schema ) { +- return $this->add_additional_fields_schema( $this->schema ); +- } +- +- $schema = array( +- '$schema' => 'http://json-schema.org/draft-04/schema#', +- 'title' => 'block-type', +- 'type' => 'object', +- 'properties' => array( +- 'title' => array( +- 'description' => __( 'Title of block type.', 'gutenberg' ), +- 'type' => 'string', +- 'default' => '', +- 'context' => array( 'embed', 'view', 'edit' ), +- 'readonly' => true, +- ), +- 'name' => array( +- 'description' => __( 'Unique name identifying the block type.', 'gutenberg' ), +- 'type' => 'string', +- 'default' => '', +- 'context' => array( 'embed', 'view', 'edit' ), +- 'readonly' => true, +- ), +- 'description' => array( +- 'description' => __( 'Description of block type.', 'gutenberg' ), +- 'type' => 'string', +- 'default' => '', +- 'context' => array( 'embed', 'view', 'edit' ), +- 'readonly' => true, +- ), +- 'icon' => array( +- 'description' => __( 'Icon of block type.', 'gutenberg' ), +- 'type' => array( 'string', 'null' ), +- 'default' => null, +- 'context' => array( 'embed', 'view', 'edit' ), +- 'readonly' => true, +- ), +- 'attributes' => array( +- 'description' => __( 'Block attributes.', 'gutenberg' ), +- 'type' => array( 'object', 'null' ), +- 'properties' => array(), +- 'default' => null, +- 'additionalProperties' => array( +- 'type' => 'object', +- ), +- 'context' => array( 'embed', 'view', 'edit' ), +- 'readonly' => true, +- ), +- 'provides_context' => array( +- 'description' => __( 'Context provided by blocks of this type.', 'gutenberg' ), +- 'type' => 'object', +- 'properties' => array(), +- 'additionalProperties' => array( +- 'type' => 'string', +- ), +- 'default' => array(), +- 'context' => array( 'embed', 'view', 'edit' ), +- 'readonly' => true, +- ), +- 'uses_context' => array( +- 'description' => __( 'Context values inherited by blocks of this type.', 'gutenberg' ), +- 'type' => 'array', +- 'default' => array(), +- 'items' => array( +- 'type' => 'string', +- ), +- 'context' => array( 'embed', 'view', 'edit' ), +- 'readonly' => true, +- ), +- 'supports' => array( +- 'description' => __( 'Block supports.', 'gutenberg' ), +- 'type' => 'object', +- 'default' => array(), +- 'properties' => array(), +- 'context' => array( 'embed', 'view', 'edit' ), +- 'readonly' => true, +- ), +- 'category' => array( +- 'description' => __( 'Block category.', 'gutenberg' ), +- 'type' => array( 'string', null ), +- 'default' => null, +- 'context' => array( 'embed', 'view', 'edit' ), +- 'readonly' => true, +- ), +- 'is_dynamic' => array( +- 'description' => __( 'Is the block dynamically rendered.', 'gutenberg' ), +- 'type' => 'boolean', +- 'default' => false, +- 'context' => array( 'embed', 'view', 'edit' ), +- 'readonly' => true, +- ), +- 'editor_script' => array( +- 'description' => __( 'Editor script handle.', 'gutenberg' ), +- 'type' => array( 'string', null ), +- 'default' => null, +- 'context' => array( 'embed', 'view', 'edit' ), +- 'readonly' => true, +- ), +- 'script' => array( +- 'description' => __( 'Public facing script handle.', 'gutenberg' ), +- 'type' => array( 'string', null ), +- 'default' => null, +- 'context' => array( 'embed', 'view', 'edit' ), +- 'readonly' => true, +- ), +- 'editor_style' => array( +- 'description' => __( 'Editor style handle.', 'gutenberg' ), +- 'type' => array( 'string', null ), +- 'default' => null, +- 'context' => array( 'embed', 'view', 'edit' ), +- 'readonly' => true, +- ), +- 'style' => array( +- 'description' => __( 'Public facing style handle.', 'gutenberg' ), +- 'type' => array( 'string', null ), +- 'default' => null, +- 'context' => array( 'embed', 'view', 'edit' ), +- 'readonly' => true, +- ), +- 'styles' => array( +- 'description' => __( 'Block style variations.', 'gutenberg' ), +- 'type' => 'array', +- 'properties' => array(), +- 'additionalProperties' => array( +- 'type' => 'object', +- ), +- 'default' => array(), +- 'context' => array( 'embed', 'view', 'edit' ), +- 'readonly' => true, +- ), +- 'textdomain' => array( +- 'description' => __( 'Public text domain.', 'gutenberg' ), +- 'type' => array( 'string', 'null' ), +- 'default' => null, +- 'context' => array( 'embed', 'view', 'edit' ), +- 'readonly' => true, +- ), +- 'parent' => array( +- 'description' => __( 'Parent blocks.', 'gutenberg' ), +- 'type' => array( 'array', 'null' ), +- 'items' => array( +- 'type' => 'string', +- ), +- 'default' => null, +- 'context' => array( 'embed', 'view', 'edit' ), +- 'readonly' => true, +- ), +- 'keywords' => array( +- 'description' => __( 'Block keywords.', 'gutenberg' ), +- 'type' => 'array', +- 'items' => array( +- 'type' => 'string', +- ), +- 'default' => array(), +- 'context' => array( 'embed', 'view', 'edit' ), +- 'readonly' => true, +- ), +- 'example' => array( +- 'description' => __( 'Block example.', 'gutenberg' ), +- 'type' => array( 'object', 'null' ), +- 'default' => null, +- 'properties' => array(), +- 'additionalProperties' => array( +- 'type' => 'object', +- ), +- 'context' => array( 'embed', 'view', 'edit' ), +- 'readonly' => true, +- ), +- ), +- ); +- +- $this->schema = $schema; +- +- return $this->add_additional_fields_schema( $this->schema ); +- } +- +- /** +- * Retrieves the query params for collections. +- * +- * @return array Collection parameters. +- */ +- public function get_collection_params() { +- $new_params = array(); +- $new_params['context'] = $this->get_context_param( array( 'default' => 'view' ) ); +- $new_params['namespace'] = array( +- 'description' => __( 'Block namespace.', 'gutenberg' ), +- 'type' => 'string', +- ); +- return $new_params; +- } +- +-} +Skip. Customizer nonce endpoint is experimental. +diff --git a/lib/class-wp-rest-customizer-nonces.php b/lib/class-wp-rest-customizer-nonces.php +index 1eab33cf2e..4fc35209ae 100644 +--- a/lib/class-wp-rest-customizer-nonces.php ++++ b/lib/class-wp-rest-customizer-nonces.php +@@ -43,10 +43,9 @@ class WP_Rest_Customizer_Nonces extends WP_REST_Controller { + /** + * Checks if a given request has access to read menu items if they have access to edit them. + * +- * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. + */ +- public function permissions_check( $request ) { ++ public function permissions_check() { + $post_type = get_post_type_object( 'nav_menu_item' ); + if ( ! current_user_can( $post_type->cap->edit_posts ) ) { + return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit posts in this post type.', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) ); +Skip. Minimum version bump. I asume this is already in Core? Couldn't find it. +diff --git a/lib/class-wp-rest-image-editor-controller.php b/lib/class-wp-rest-image-editor-controller.php +deleted file mode 100644 +index 66c8531e02..0000000000 +--- a/lib/class-wp-rest-image-editor-controller.php ++++ /dev/null +@@ -1,362 +0,0 @@ +-namespace = 'wp/v2'; +- $this->rest_base = 'media'; +- } +- +- /** +- * Registers the necessary REST API routes. +- * +- * @since 7.x ? +- * @access public +- */ +- public function register_routes() { +- register_rest_route( +- $this->namespace, +- '/' . $this->rest_base . '/(?P[\d]+)/edit', +- array( +- array( +- 'methods' => WP_REST_Server::EDITABLE, +- 'callback' => array( $this, 'apply_edits' ), +- 'permission_callback' => array( $this, 'permission_callback' ), +- 'args' => array( +- 'rotation' => array( +- 'type' => 'integer', +- ), +- +- // Src is required to check for correct $image_meta. +- 'src' => array( +- 'type' => 'string', +- 'required' => true, +- ), +- +- // Crop values are in percents. +- 'x' => array( +- 'type' => 'number', +- 'minimum' => 0, +- 'maximum' => 100, +- ), +- 'y' => array( +- 'type' => 'number', +- 'minimum' => 0, +- 'maximum' => 100, +- ), +- 'width' => array( +- 'type' => 'number', +- 'minimum' => 0, +- 'maximum' => 100, +- ), +- 'height' => array( +- 'type' => 'number', +- 'minimum' => 0, +- 'maximum' => 100, +- ), +- ), +- ), +- ) +- ); +- } +- +- /** +- * Checks if the user has permissions to make the request. +- * +- * @since 7.x ? +- * @access public +- * +- * @param WP_REST_Request $request Full details about the request. +- * @return true|WP_Error True if the request has read access, WP_Error object otherwise. +- */ +- public function permission_callback( $request ) { +- if ( ! current_user_can( 'edit_post', $request['id'] ) ) { +- $error = __( 'Sorry, you are not allowed to edit images.', 'gutenberg' ); +- return new WP_Error( 'rest_cannot_edit_image', $error, array( 'status' => rest_authorization_required_code() ) ); +- } +- +- if ( ! current_user_can( 'upload_files' ) ) { +- return new WP_Error( 'rest_cannot_edit_image', __( 'Sorry, you are not allowed to upload media on this site.', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) ); +- } +- +- return true; +- } +- +- /** +- * Applies all edits in one go. +- * +- * @since 7.x ? +- * @access public +- * +- * @param WP_REST_Request $request Full details about the request. +- * @return WP_REST_Response|WP_Error If successful image JSON for the modified image, otherwise a WP_Error. +- */ +- public function apply_edits( $request ) { +- require_once ABSPATH . 'wp-admin/includes/image.php'; +- +- $attachment_id = $request['id']; +- +- // This also confirms the attachment is an image. +- $image_file = wp_get_original_image_path( $attachment_id ); +- $image_meta = wp_get_attachment_metadata( $attachment_id ); +- +- if ( function_exists( 'wp_image_file_matches_image_meta' ) ) { +- if ( +- ! $image_meta || +- ! $image_file || +- ! wp_image_file_matches_image_meta( $request['src'], $image_meta ) +- ) { +- return new WP_Error( +- 'rest_unknown_attachment', +- __( 'Unable to get meta information for file.', 'gutenberg' ), +- array( 'status' => 404 ) +- ); +- } +- } else { +- // Back-compat for WP versions < 5.5. +- if ( ! $image_meta || ! $image_file ) { +- return new WP_Error( +- 'rest_unknown_attachment', +- __( 'Unable to get meta information for file.', 'gutenberg' ), +- array( 'status' => 404 ) +- ); +- } else { +- $match = false; +- $image_src = $request['src']; +- +- if ( isset( $image_meta['file'] ) && strlen( $image_meta['file'] ) > 4 ) { +- // Remove quiery args. +- list( $image_src ) = explode( '?', $image_src ); +- +- // Check if the relative image path from the image meta is at the end of $image_src. +- if ( strrpos( $image_src, $image_meta['file'] ) === strlen( $image_src ) - strlen( $image_meta['file'] ) ) { +- $match = true; +- } +- +- if ( ! empty( $image_meta['sizes'] ) ) { +- // Retrieve the uploads sub-directory from the full size image. +- $dirname = _wp_get_attachment_relative_path( $image_meta['file'] ); +- +- if ( $dirname ) { +- $dirname = trailingslashit( $dirname ); +- } +- +- foreach ( $image_meta['sizes'] as $image_size_data ) { +- $relative_path = $dirname . $image_size_data['file']; +- +- if ( strrpos( $image_src, $relative_path ) === strlen( $image_src ) - strlen( $relative_path ) ) { +- $match = true; +- break; +- } +- } +- } +- } +- +- if ( ! $match ) { +- return new WP_Error( +- 'rest_unknown_attachment', +- __( 'Unable to get meta information for file.', 'gutenberg' ), +- array( 'status' => 404 ) +- ); +- } +- } +- } +- +- $supported_types = array( 'image/jpeg', 'image/png', 'image/gif' ); +- $mime_type = get_post_mime_type( $attachment_id ); +- if ( ! in_array( $mime_type, $supported_types, true ) ) { +- return new WP_Error( +- 'rest_cannot_edit_file_type', +- __( 'This type of file cannot be edited.', 'gutenberg' ), +- array( 'status' => 400 ) +- ); +- } +- +- // Check if we need to do anything. +- $rotate = 0; +- $crop = false; +- +- if ( ! empty( $request['rotation'] ) ) { +- // Rotation direction: clockwise vs. counter clockwise. +- $rotate = 0 - (int) $request['rotation']; +- } +- +- if ( isset( $request['x'], $request['y'], $request['width'], $request['height'] ) ) { +- $crop = true; +- } +- +- if ( ! $rotate && ! $crop ) { +- $error = __( 'The image was not edited. Edit the image before applying the changes.', 'gutenberg' ); +- return new WP_Error( 'rest_image_not_edited', $error, array( 'status' => 400 ) ); +- } +- +- // If the file doesn't exist, attempt a URL fopen on the src link. +- // This can occur with certain file replication plugins. +- // Keep the original file path to get a modified name later. +- $image_file_to_edit = $image_file; +- if ( ! file_exists( $image_file_to_edit ) ) { +- $image_file_to_edit = _load_image_to_edit_path( $attachment_id ); +- } +- +- $image_editor = wp_get_image_editor( $image_file_to_edit ); +- +- if ( is_wp_error( $image_editor ) ) { +- // This image cannot be edited. +- $error = __( 'Unable to edit this image.', 'gutenberg' ); +- return new WP_Error( 'rest_unknown_image_file_type', $error, array( 'status' => 500 ) ); +- } +- +- if ( 0 !== $rotate ) { +- $result = $image_editor->rotate( $rotate ); +- +- if ( is_wp_error( $result ) ) { +- $error = __( 'Unable to rotate this image.', 'gutenberg' ); +- return new WP_Error( 'rest_image_rotation_failed', $error, array( 'status' => 500 ) ); +- } +- } +- +- if ( $crop ) { +- $size = $image_editor->get_size(); +- +- $crop_x = round( ( $size['width'] * floatval( $request['x'] ) ) / 100.0 ); +- $crop_y = round( ( $size['height'] * floatval( $request['y'] ) ) / 100.0 ); +- $width = round( ( $size['width'] * floatval( $request['width'] ) ) / 100.0 ); +- $height = round( ( $size['height'] * floatval( $request['height'] ) ) / 100.0 ); +- +- $result = $image_editor->crop( $crop_x, $crop_y, $width, $height ); +- +- if ( is_wp_error( $result ) ) { +- $error = __( 'Unable to crop this image.', 'gutenberg' ); +- return new WP_Error( 'rest_image_crop_failed', $error, array( 'status' => 500 ) ); +- } +- } +- +- // Calculate the file name. +- $image_ext = pathinfo( $image_file, PATHINFO_EXTENSION ); +- $image_name = wp_basename( $image_file, ".{$image_ext}" ); +- +- // Do not append multiple `-edited` to the file name. +- // The user may be editing a previously edited image. +- if ( preg_match( '/-edited(-\d+)?$/', $image_name ) ) { +- // Remove any `-1`, `-2`, etc. `wp_unique_filename()` will add the proper number. +- $image_name = preg_replace( '/-edited(-\d+)?$/', '-edited', $image_name ); +- } else { +- // Append `-edited` before the extension. +- $image_name .= '-edited'; +- } +- +- $filename = "{$image_name}.{$image_ext}"; +- +- // Create the uploads sub-directory if needed. +- $uploads = wp_upload_dir(); +- +- // Make the file name unique in the (new) upload directory. +- $filename = wp_unique_filename( $uploads['path'], $filename ); +- +- // Save to disk. +- $saved = $image_editor->save( $uploads['path'] . "/$filename" ); +- +- if ( is_wp_error( $saved ) ) { +- return $saved; +- } +- +- // Create new attachment post. +- $attachment_post = array( +- 'post_mime_type' => $saved['mime-type'], +- 'guid' => $uploads['url'] . "/$filename", +- 'post_title' => $filename, +- 'post_content' => '', +- ); +- +- $new_attachment_id = wp_insert_attachment( wp_slash( $attachment_post ), $saved['path'], 0, true ); +- +- if ( is_wp_error( $new_attachment_id ) ) { +- if ( 'db_update_error' === $new_attachment_id->get_error_code() ) { +- $new_attachment_id->add_data( array( 'status' => 500 ) ); +- } else { +- $new_attachment_id->add_data( array( 'status' => 400 ) ); +- } +- +- return $new_attachment_id; +- } +- +- if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { +- // Set a custom header with the attachment_id. +- // Used by the browser/client to resume creating image sub-sizes after a PHP fatal error. +- header( 'X-WP-Upload-Attachment-ID: ' . $new_attachment_id ); +- } +- +- // Generate image sub-sizes and meta. +- $new_image_meta = wp_generate_attachment_metadata( $new_attachment_id, $saved['path'] ); +- +- // Copy the EXIF metadata from the original attachment if not generated for the edited image. +- if ( ! empty( $image_meta['image_meta'] ) ) { +- $empty_image_meta = true; +- +- if ( isset( $new_image_meta['image_meta'] ) && is_array( $new_image_meta['image_meta'] ) ) { +- $empty_image_meta = empty( array_filter( array_values( $new_image_meta['image_meta'] ) ) ); +- } +- +- if ( $empty_image_meta ) { +- $new_image_meta['image_meta'] = $image_meta['image_meta']; +- } +- } +- +- // Reset orientation. At this point the image is edited and orientation is correct. +- if ( ! empty( $new_image_meta['image_meta']['orientation'] ) ) { +- $new_image_meta['image_meta']['orientation'] = 1; +- } +- +- // The attachment_id may change if the site is exported and imported. +- $new_image_meta['parent_image'] = array( +- 'attachment_id' => $attachment_id, +- // Path to the originally uploaded image file relative to the uploads directory. +- 'file' => _wp_relative_upload_path( $image_file ), +- ); +- +- /** +- * Filters the updated attachment meta data. +- * +- * @since 5.5.0 +- * +- * @param array $data Array of updated attachment meta data. +- * @param int $new_attachment_id Attachment post ID. +- * @param int $attachment_id Original Attachment post ID. +- */ +- $new_image_meta = apply_filters( 'wp_edited_attachment_metadata', $new_image_meta, $new_attachment_id, $attachment_id ); +- +- wp_update_attachment_metadata( $new_attachment_id, $new_image_meta ); +- +- $path = '/wp/v2/media/' . $new_attachment_id; +- $new_request = new WP_REST_Request( 'GET', $path ); +- $new_request->set_query_params( array( 'context' => 'edit' ) ); +- $response = rest_do_request( $new_request ); +- +- if ( ! $response->is_error() ) { +- $response->set_status( 201 ); +- $response->header( 'Location', rest_url( $path ) ); +- } +- +- return $response; +- } +-} +Skip. Minimum version bump. +diff --git a/lib/class-wp-rest-plugins-controller.php b/lib/class-wp-rest-plugins-controller.php +deleted file mode 100644 +index 49a55192da..0000000000 +--- a/lib/class-wp-rest-plugins-controller.php ++++ /dev/null +@@ -1,950 +0,0 @@ +-namespace = 'wp/v2'; +- $this->rest_base = 'plugins'; +- } +- +- /** +- * Registers the routes for the plugins controller. +- * +- * @since 5.5.0 +- */ +- public function register_routes() { +- register_rest_route( +- $this->namespace, +- '/' . $this->rest_base, +- array( +- array( +- 'methods' => WP_REST_Server::READABLE, +- 'callback' => array( $this, 'get_items' ), +- 'permission_callback' => array( $this, 'get_items_permissions_check' ), +- 'args' => $this->get_collection_params(), +- ), +- array( +- 'methods' => WP_REST_Server::CREATABLE, +- 'callback' => array( $this, 'create_item' ), +- 'permission_callback' => array( $this, 'create_item_permissions_check' ), +- 'args' => array( +- 'slug' => array( +- 'type' => 'string', +- 'required' => true, +- 'description' => __( 'WordPress.org plugin directory slug.', 'gutenberg' ), +- 'pattern' => '[\w\-]+', +- ), +- 'status' => array( +- 'description' => __( 'The plugin activation status.', 'gutenberg' ), +- 'type' => 'string', +- 'enum' => is_multisite() ? array( 'inactive', 'active', 'network-active' ) : array( 'inactive', 'active' ), +- 'default' => 'inactive', +- ), +- ), +- ), +- 'schema' => array( $this, 'get_public_item_schema' ), +- ) +- ); +- +- register_rest_route( +- $this->namespace, +- '/' . $this->rest_base . '/(?P' . self::PATTERN . ')', +- array( +- array( +- 'methods' => WP_REST_Server::READABLE, +- 'callback' => array( $this, 'get_item' ), +- 'permission_callback' => array( $this, 'get_item_permissions_check' ), +- ), +- array( +- 'methods' => WP_REST_Server::EDITABLE, +- 'callback' => array( $this, 'update_item' ), +- 'permission_callback' => array( $this, 'update_item_permissions_check' ), +- 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), +- ), +- array( +- 'methods' => WP_REST_Server::DELETABLE, +- 'callback' => array( $this, 'delete_item' ), +- 'permission_callback' => array( $this, 'delete_item_permissions_check' ), +- ), +- 'args' => array( +- 'context' => $this->get_context_param( array( 'default' => 'view' ) ), +- 'plugin' => array( +- 'type' => 'string', +- 'pattern' => self::PATTERN, +- 'validate_callback' => array( $this, 'validate_plugin_param' ), +- 'sanitize_callback' => array( $this, 'sanitize_plugin_param' ), +- ), +- ), +- 'schema' => array( $this, 'get_public_item_schema' ), +- ) +- ); +- } +- +- /** +- * Checks if a given request has access to get plugins. +- * +- * @since 5.5.0 +- * +- * @param WP_REST_Request $request Full details about the request. +- * @return true|WP_Error True if the request has read access, WP_Error object otherwise. +- */ +- public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable +- if ( ! current_user_can( 'activate_plugins' ) ) { +- return new WP_Error( +- 'rest_cannot_view_plugins', +- __( 'Sorry, you are not allowed to manage plugins for this site.', 'gutenberg' ), +- array( 'status' => rest_authorization_required_code() ) +- ); +- } +- +- return true; +- } +- +- /** +- * Retrieves a collection of plugins. +- * +- * @since 5.5.0 +- * +- * @param WP_REST_Request $request Full details about the request. +- * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. +- */ +- public function get_items( $request ) { +- require_once ABSPATH . 'wp-admin/includes/plugin.php'; +- +- $plugins = array(); +- +- foreach ( get_plugins() as $file => $data ) { +- if ( is_wp_error( $this->check_read_permission( $file ) ) ) { +- continue; +- } +- +- $data['_file'] = $file; +- +- if ( ! $this->does_plugin_match_request( $request, $data ) ) { +- continue; +- } +- +- $plugins[] = $this->prepare_response_for_collection( $this->prepare_item_for_response( $data, $request ) ); +- } +- +- return new WP_REST_Response( $plugins ); +- } +- +- /** +- * Checks if a given request has access to get a specific plugin. +- * +- * @since 5.5.0 +- * +- * @param WP_REST_Request $request Full details about the request. +- * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise. +- */ +- public function get_item_permissions_check( $request ) { +- if ( ! current_user_can( 'activate_plugins' ) ) { +- return new WP_Error( +- 'rest_cannot_view_plugin', +- __( 'Sorry, you are not allowed to manage plugins for this site.', 'gutenberg' ), +- array( 'status' => rest_authorization_required_code() ) +- ); +- } +- +- $can_read = $this->check_read_permission( $request['plugin'] ); +- +- if ( is_wp_error( $can_read ) ) { +- return $can_read; +- } +- +- return true; +- } +- +- /** +- * Retrieves one plugin from the site. +- * +- * @since 5.5.0 +- * +- * @param WP_REST_Request $request Full details about the request. +- * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. +- */ +- public function get_item( $request ) { +- require_once ABSPATH . 'wp-admin/includes/plugin.php'; +- +- $data = $this->get_plugin_data( $request['plugin'] ); +- +- if ( is_wp_error( $data ) ) { +- return $data; +- } +- +- return $this->prepare_item_for_response( $data, $request ); +- } +- +- /** +- * Checks if the given plugin can be viewed by the current user. +- * +- * On multisite, this hides non-active network only plugins if the user does not have permission +- * to manage network plugins. +- * +- * @since 5.5.0 +- * +- * @param string $plugin The plugin file to check. +- * @return true|WP_Error True if can read, a WP_Error instance otherwise. +- */ +- protected function check_read_permission( $plugin ) { +- if ( ! $this->is_plugin_installed( $plugin ) ) { +- return new WP_Error( 'rest_plugin_not_found', __( 'Plugin not found.', 'gutenberg' ), array( 'status' => 404 ) ); +- } +- +- if ( ! is_multisite() ) { +- return true; +- } +- +- if ( ! is_network_only_plugin( $plugin ) || is_plugin_active( $plugin ) || current_user_can( 'manage_network_plugins' ) ) { +- return true; +- } +- +- return new WP_Error( +- 'rest_cannot_view_plugin', +- __( 'Sorry, you are not allowed to manage this plugin.', 'gutenberg' ), +- array( 'status' => rest_authorization_required_code() ) +- ); +- } +- +- /** +- * Checks if a given request has access to upload plugins. +- * +- * @since 5.5.0 +- * +- * @param WP_REST_Request $request Full details about the request. +- * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise. +- */ +- public function create_item_permissions_check( $request ) { +- if ( ! current_user_can( 'install_plugins' ) ) { +- return new WP_Error( +- 'rest_cannot_install_plugin', +- __( 'Sorry, you are not allowed to install plugins on this site.', 'gutenberg' ), +- array( 'status' => rest_authorization_required_code() ) +- ); +- } +- +- if ( 'inactive' !== $request['status'] && ! current_user_can( 'activate_plugins' ) ) { +- return new WP_Error( +- 'rest_cannot_activate_plugin', +- __( 'Sorry, you are not allowed to activate plugins.', 'gutenberg' ), +- array( +- 'status' => rest_authorization_required_code(), +- ) +- ); +- } +- +- return true; +- } +- +- /** +- * Uploads a plugin and optionally activates it. +- * +- * @since 5.5.0 +- * +- * @param WP_REST_Request $request Full details about the request. +- * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. +- */ +- public function create_item( $request ) { +- require_once ABSPATH . 'wp-admin/includes/file.php'; +- require_once ABSPATH . 'wp-admin/includes/plugin.php'; +- require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; +- require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; +- +- $slug = $request['slug']; +- +- // Verify filesystem is accessible first. +- $filesystem_available = $this->is_filesystem_available(); +- if ( is_wp_error( $filesystem_available ) ) { +- return $filesystem_available; +- } +- +- $api = plugins_api( +- 'plugin_information', +- array( +- 'slug' => $slug, +- 'fields' => array( +- 'sections' => false, +- ), +- ) +- ); +- +- if ( is_wp_error( $api ) ) { +- if ( false !== strpos( $api->get_error_message(), 'Plugin not found.' ) ) { +- $api->add_data( array( 'status' => 404 ) ); +- } else { +- $api->add_data( array( 'status' => 500 ) ); +- } +- +- return $api; +- } +- +- $skin = new WP_Ajax_Upgrader_Skin(); +- $upgrader = new Plugin_Upgrader( $skin ); +- +- $result = $upgrader->install( $api->download_link ); +- +- if ( is_wp_error( $result ) ) { +- $result->add_data( array( 'status' => 500 ) ); +- +- return $result; +- } +- +- // This should be the same as $result above. +- if ( is_wp_error( $skin->result ) ) { +- $skin->result->add_data( array( 'status' => 500 ) ); +- +- return $skin->result; +- } +- +- if ( $skin->get_errors()->has_errors() ) { +- $error = $skin->get_errors(); +- $error->add_data( array( 'status' => 500 ) ); +- +- return $error; +- } +- +- if ( is_null( $result ) ) { +- global $wp_filesystem; +- // Pass through the error from WP_Filesystem if one was raised. +- if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) { +- return new WP_Error( 'unable_to_connect_to_filesystem', $wp_filesystem->errors->get_error_message(), array( 'status' => 500 ) ); +- } +- +- return new WP_Error( 'unable_to_connect_to_filesystem', __( 'Unable to connect to the filesystem. Please confirm your credentials.', 'gutenberg' ), array( 'status' => 500 ) ); +- } +- +- $file = $upgrader->plugin_info(); +- +- if ( ! $file ) { +- return new WP_Error( 'unable_to_determine_installed_plugin', __( 'Unable to determine what plugin was installed.', 'gutenberg' ), array( 'status' => 500 ) ); +- } +- +- if ( 'inactive' !== $request['status'] ) { +- $can_change_status = $this->plugin_status_permission_check( $file, $request['status'], 'inactive' ); +- +- if ( is_wp_error( $can_change_status ) ) { +- return $can_change_status; +- } +- +- $changed_status = $this->handle_plugin_status( $file, $request['status'], 'inactive' ); +- +- if ( is_wp_error( $changed_status ) ) { +- return $changed_status; +- } +- } +- +- $path = WP_PLUGIN_DIR . '/' . $file; +- $data = get_plugin_data( $path, false, false ); +- $data['_file'] = $file; +- +- $response = $this->prepare_item_for_response( $data, $request ); +- $response->set_status( 201 ); +- $response->header( 'Location', rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, substr( $file, 0, - 4 ) ) ) ); +- +- return $response; +- } +- +- /** +- * Checks if a given request has access to update a specific plugin. +- * +- * @since 5.5.0 +- * +- * @param WP_REST_Request $request Full details about the request. +- * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise. +- */ +- public function update_item_permissions_check( $request ) { +- require_once ABSPATH . 'wp-admin/includes/plugin.php'; +- +- if ( ! current_user_can( 'activate_plugins' ) ) { +- return new WP_Error( +- 'rest_cannot_manage_plugins', +- __( 'Sorry, you are not allowed to manage plugins for this site.', 'gutenberg' ), +- array( 'status' => rest_authorization_required_code() ) +- ); +- } +- +- $can_read = $this->check_read_permission( $request['plugin'] ); +- +- if ( is_wp_error( $can_read ) ) { +- return $can_read; +- } +- +- $status = $this->get_plugin_status( $request['plugin'] ); +- +- if ( $request['status'] && $status !== $request['status'] ) { +- $can_change_status = $this->plugin_status_permission_check( $request['plugin'], $request['status'], $status ); +- +- if ( is_wp_error( $can_change_status ) ) { +- return $can_change_status; +- } +- } +- +- return true; +- } +- +- /** +- * Updates one plugin. +- * +- * @since 5.5.0 +- * +- * @param WP_REST_Request $request Full details about the request. +- * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. +- */ +- public function update_item( $request ) { +- require_once ABSPATH . 'wp-admin/includes/plugin.php'; +- +- $data = $this->get_plugin_data( $request['plugin'] ); +- +- if ( is_wp_error( $data ) ) { +- return $data; +- } +- +- $status = $this->get_plugin_status( $request['plugin'] ); +- +- if ( $request['status'] && $status !== $request['status'] ) { +- $handled = $this->handle_plugin_status( $request['plugin'], $request['status'], $status ); +- +- if ( is_wp_error( $handled ) ) { +- return $handled; +- } +- } +- +- $this->update_additional_fields_for_object( $data, $request ); +- +- $request['context'] = 'edit'; +- +- return $this->prepare_item_for_response( $data, $request ); +- } +- +- /** +- * Checks if a given request has access to delete a specific plugin. +- * +- * @since 5.5.0 +- * +- * @param WP_REST_Request $request Full details about the request. +- * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise. +- */ +- public function delete_item_permissions_check( $request ) { +- if ( ! current_user_can( 'activate_plugins' ) ) { +- return new WP_Error( +- 'rest_cannot_manage_plugins', +- __( 'Sorry, you are not allowed to manage plugins for this site.', 'gutenberg' ), +- array( 'status' => rest_authorization_required_code() ) +- ); +- } +- +- if ( ! current_user_can( 'delete_plugins' ) ) { +- return new WP_Error( +- 'rest_cannot_manage_plugins', +- __( 'Sorry, you are not allowed to delete plugins for this site.', 'gutenberg' ), +- array( 'status' => rest_authorization_required_code() ) +- ); +- } +- +- $can_read = $this->check_read_permission( $request['plugin'] ); +- +- if ( is_wp_error( $can_read ) ) { +- return $can_read; +- } +- +- return true; +- } +- +- /** +- * Deletes one plugin from the site. +- * +- * @since 5.5.0 +- * +- * @param WP_REST_Request $request Full details about the request. +- * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. +- */ +- public function delete_item( $request ) { +- require_once ABSPATH . 'wp-admin/includes/file.php'; +- require_once ABSPATH . 'wp-admin/includes/plugin.php'; +- +- $data = $this->get_plugin_data( $request['plugin'] ); +- +- if ( is_wp_error( $data ) ) { +- return $data; +- } +- +- if ( is_plugin_active( $request['plugin'] ) ) { +- return new WP_Error( +- 'rest_cannot_delete_active_plugin', +- __( 'Cannot delete an active plugin. Please deactivate it first.', 'gutenberg' ), +- array( 'status' => 400 ) +- ); +- } +- +- $filesystem_available = $this->is_filesystem_available(); +- if ( is_wp_error( $filesystem_available ) ) { +- return $filesystem_available; +- } +- +- $prepared = $this->prepare_item_for_response( $data, $request ); +- $deleted = delete_plugins( array( $request['plugin'] ) ); +- +- if ( is_wp_error( $deleted ) ) { +- $deleted->add_data( array( 'status' => 500 ) ); +- +- return $deleted; +- } +- +- return new WP_REST_Response( +- array( +- 'deleted' => true, +- 'previous' => $prepared->get_data(), +- ) +- ); +- } +- +- /** +- * Prepares the plugin for the REST response. +- * +- * @since 5.5.0 +- * +- * @param mixed $item Unmarked up and untranslated plugin data from {@see get_plugin_data()}. +- * @param WP_REST_Request $request Request object. +- * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. +- */ +- public function prepare_item_for_response( $item, $request ) { +- $item = _get_plugin_data_markup_translate( $item['_file'], $item, false ); +- $marked = _get_plugin_data_markup_translate( $item['_file'], $item, true ); +- +- $data = array( +- 'plugin' => substr( $item['_file'], 0, - 4 ), +- 'status' => $this->get_plugin_status( $item['_file'] ), +- 'name' => $item['Name'], +- 'plugin_uri' => $item['PluginURI'], +- 'author' => $item['Author'], +- 'author_uri' => $item['AuthorURI'], +- 'description' => array( +- 'raw' => $item['Description'], +- 'rendered' => $marked['Description'], +- ), +- 'version' => $item['Version'], +- 'network_only' => $item['Network'], +- 'requires_wp' => $item['RequiresWP'], +- 'requires_php' => $item['RequiresPHP'], +- 'text_domain' => $item['TextDomain'], +- ); +- +- $data = $this->add_additional_fields_to_object( $data, $request ); +- +- $response = new WP_REST_Response( $data ); +- $response->add_links( $this->prepare_links( $item ) ); +- +- /** +- * Filters the plugin data for a response. +- * +- * @since 5.5.0 +- * +- * @param WP_REST_Response $response The response object. +- * @param array $item The plugin item from {@see get_plugin_data()}. +- * @param WP_REST_Request $request The request object. +- */ +- return apply_filters( 'rest_prepare_plugin', $response, $item, $request ); +- } +- +- /** +- * Prepares links for the request. +- * +- * @since 5.5.0 +- * +- * @param array $item The plugin item. +- * @return array[] +- */ +- protected function prepare_links( $item ) { +- return array( +- 'self' => array( +- 'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, substr( $item['_file'], 0, - 4 ) ) ), +- ), +- ); +- } +- +- /** +- * Gets the plugin header data for a plugin. +- * +- * @since 5.5.0 +- * +- * @param string $plugin The plugin file to get data for. +- * @return array|WP_Error The plugin data, or a WP_Error if the plugin is not installed. +- */ +- protected function get_plugin_data( $plugin ) { +- $plugins = get_plugins(); +- +- if ( ! isset( $plugins[ $plugin ] ) ) { +- return new WP_Error( 'rest_plugin_not_found', __( 'Plugin not found.', 'gutenberg' ), array( 'status' => 404 ) ); +- } +- +- $data = $plugins[ $plugin ]; +- $data['_file'] = $plugin; +- +- return $data; +- } +- +- /** +- * Get's the activation status for a plugin. +- * +- * @since 5.5.0 +- * +- * @param string $plugin The plugin file to check. +- * @return string Either 'network-active', 'active' or 'inactive'. +- */ +- protected function get_plugin_status( $plugin ) { +- if ( is_plugin_active_for_network( $plugin ) ) { +- return 'network-active'; +- } +- +- if ( is_plugin_active( $plugin ) ) { +- return 'active'; +- } +- +- return 'inactive'; +- } +- +- /** +- * Handle updating a plugin's status. +- * +- * @since 5.5.0 +- * +- * @param string $plugin The plugin file to update. +- * @param string $new_status The plugin's new status. +- * @param string $current_status The plugin's current status. +- * +- * @return true|WP_Error +- */ +- protected function plugin_status_permission_check( $plugin, $new_status, $current_status ) { +- if ( is_multisite() && ( 'network-active' === $current_status || 'network-active' === $new_status ) && ! current_user_can( 'manage_network_plugins' ) ) { +- return new WP_Error( +- 'rest_cannot_manage_network_plugins', +- __( 'Sorry, you do not have permission to manage network plugins.', 'gutenberg' ), +- array( 'status' => rest_authorization_required_code() ) +- ); +- } +- +- if ( ( 'active' === $new_status || 'network-active' === $new_status ) && ! current_user_can( 'activate_plugin', $plugin ) ) { +- return new WP_Error( +- 'rest_cannot_activate_plugin', +- __( 'Sorry, you are not allowed to activate this plugin.', 'gutenberg' ), +- array( 'status' => rest_authorization_required_code() ) +- ); +- } +- +- if ( 'inactive' === $new_status && ! current_user_can( 'deactivate_plugin', $plugin ) ) { +- return new WP_Error( +- 'rest_cannot_deactivate_plugin', +- __( 'Sorry, you are not allowed to deactivate this plugin.', 'gutenberg' ), +- array( 'status' => rest_authorization_required_code() ) +- ); +- } +- +- return true; +- } +- +- /** +- * Handle updating a plugin's status. +- * +- * @since 5.5.0 +- * +- * @param string $plugin The plugin file to update. +- * @param string $new_status The plugin's new status. +- * @param string $current_status The plugin's current status. +- * @return true|WP_Error +- */ +- protected function handle_plugin_status( $plugin, $new_status, $current_status ) { +- if ( 'inactive' === $new_status ) { +- deactivate_plugins( $plugin, false, 'network-active' === $current_status ); +- +- return true; +- } +- +- if ( 'active' === $new_status && 'network-active' === $current_status ) { +- return true; +- } +- +- $network_activate = 'network-active' === $new_status; +- +- if ( is_multisite() && ! $network_activate && is_network_only_plugin( $plugin ) ) { +- return new WP_Error( +- 'rest_network_only_plugin', +- __( 'Network only plugin must be network activated.', 'gutenberg' ), +- array( 'status' => 400 ) +- ); +- } +- +- $activated = activate_plugin( $plugin, '', $network_activate ); +- +- if ( is_wp_error( $activated ) ) { +- $activated->add_data( array( 'status' => 500 ) ); +- +- return $activated; +- } +- +- return true; +- } +- +- /** +- * Checks that the "plugin" parameter is a valid path. +- * +- * @since 5.5.0 +- * +- * @param string $file The plugin file parameter. +- * @return bool +- */ +- public function validate_plugin_param( $file ) { +- if ( ! is_string( $file ) || ! preg_match( '/' . self::PATTERN . '/u', $file ) ) { +- return false; +- } +- +- $validated = validate_file( plugin_basename( $file ) ); +- +- return 0 === $validated; +- } +- +- /** +- * Sanitizes the "plugin" parameter to be a proper plugin file with ".php" appended. +- * +- * @since 5.5.0 +- * +- * @param string $file The plugin file parameter. +- * @return string +- */ +- public function sanitize_plugin_param( $file ) { +- return plugin_basename( sanitize_text_field( $file . '.php' ) ); +- } +- +- /** +- * Checks if the plugin matches the requested parameters. +- * +- * @since 5.5.0 +- * +- * @param WP_REST_Request $request The request to require the plugin matches against. +- * @param array $item The plugin item. +- * +- * @return bool +- */ +- protected function does_plugin_match_request( $request, $item ) { +- $search = $request['search']; +- +- if ( $search ) { +- $matched_search = false; +- +- foreach ( $item as $field ) { +- if ( is_string( $field ) && false !== strpos( strip_tags( $field ), $search ) ) { +- $matched_search = true; +- break; +- } +- } +- +- if ( ! $matched_search ) { +- return false; +- } +- } +- +- $status = $request['status']; +- +- if ( $status && ! in_array( $this->get_plugin_status( $item['_file'] ), $status, true ) ) { +- return false; +- } +- +- return true; +- } +- +- /** +- * Checks if the plugin is installed. +- * +- * @since 5.5.0 +- * +- * @param string $plugin The plugin file. +- * @return bool +- */ +- protected function is_plugin_installed( $plugin ) { +- return file_exists( WP_PLUGIN_DIR . '/' . $plugin ); +- } +- +- /** +- * Determine if the endpoints are available. +- * +- * Only the 'Direct' filesystem transport, and SSH/FTP when credentials are stored are supported at present. +- * +- * @since 5.5.0 +- * +- * @return true|WP_Error True if filesystem is available, WP_Error otherwise. +- */ +- protected function is_filesystem_available() { +- $filesystem_method = get_filesystem_method(); +- +- if ( 'direct' === $filesystem_method ) { +- return true; +- } +- +- ob_start(); +- $filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() ); +- ob_end_clean(); +- +- if ( $filesystem_credentials_are_stored ) { +- return true; +- } +- +- return new WP_Error( 'fs_unavailable', __( 'The filesystem is currently unavailable for managing plugins.', 'gutenberg' ), array( 'status' => 500 ) ); +- } +- +- /** +- * Retrieves the plugin's schema, conforming to JSON Schema. +- * +- * @since 4.7.0 +- * +- * @return array Item schema data. +- */ +- public function get_item_schema() { +- if ( $this->schema ) { +- return $this->add_additional_fields_schema( $this->schema ); +- } +- +- $this->schema = array( +- '$schema' => 'http://json-schema.org/draft-04/schema#', +- 'title' => 'plugin', +- 'type' => 'object', +- 'properties' => array( +- 'plugin' => array( +- 'description' => __( 'The plugin file.', 'gutenberg' ), +- 'type' => 'string', +- 'pattern' => self::PATTERN, +- 'readonly' => true, +- 'context' => array( 'view', 'edit', 'embed' ), +- ), +- 'status' => array( +- 'description' => __( 'The plugin activation status.', 'gutenberg' ), +- 'type' => 'string', +- 'enum' => is_multisite() ? array( 'inactive', 'active', 'network-active' ) : array( 'inactive', 'active' ), +- 'context' => array( 'view', 'edit', 'embed' ), +- ), +- 'name' => array( +- 'description' => __( 'The plugin name.', 'gutenberg' ), +- 'type' => 'string', +- 'readonly' => true, +- 'context' => array( 'view', 'edit', 'embed' ), +- ), +- 'plugin_uri' => array( +- 'description' => __( 'The plugin\'s website address.', 'gutenberg' ), +- 'type' => 'string', +- 'format' => 'uri', +- 'readonly' => true, +- 'context' => array( 'view', 'edit' ), +- ), +- 'author' => array( +- 'description' => __( 'The plugin author.', 'gutenberg' ), +- 'type' => 'object', +- 'readonly' => true, +- 'context' => array( 'view', 'edit' ), +- ), +- 'author_uri' => array( +- 'description' => __( 'Plugin author\'s website address.', 'gutenberg' ), +- 'type' => 'string', +- 'format' => 'uri', +- 'readonly' => true, +- 'context' => array( 'view', 'edit' ), +- ), +- 'description' => array( +- 'description' => __( 'The plugin description.', 'gutenberg' ), +- 'type' => 'object', +- 'readonly' => true, +- 'context' => array( 'view', 'edit' ), +- 'properties' => array( +- 'raw' => array( +- 'description' => __( 'The raw plugin description.', 'gutenberg' ), +- 'type' => 'string', +- ), +- 'rendered' => array( +- 'description' => __( 'The plugin description formatted for display.', 'gutenberg' ), +- 'type' => 'string', +- ), +- ), +- ), +- 'version' => array( +- 'description' => __( 'The plugin version number.', 'gutenberg' ), +- 'type' => 'string', +- 'readonly' => true, +- 'context' => array( 'view', 'edit' ), +- ), +- 'network_only' => array( +- 'description' => __( 'Whether the plugin can only be activated network-wide.', 'gutenberg' ), +- 'type' => 'boolean', +- 'readonly' => true, +- 'context' => array( 'view', 'edit', 'embed' ), +- ), +- 'requires_wp' => array( +- 'description' => __( 'Minimum required version of WordPress.', 'gutenberg' ), +- 'type' => 'string', +- 'readonly' => true, +- 'context' => array( 'view', 'edit', 'embed' ), +- ), +- 'requires_php' => array( +- 'description' => __( 'Minimum required version of PHP.', 'gutenberg' ), +- 'type' => 'string', +- 'readonly' => true, +- 'context' => array( 'view', 'edit', 'embed' ), +- ), +- 'text_domain' => array( +- 'description' => __( 'The plugin\'s text domain.', 'gutenberg' ), +- 'type' => 'string', +- 'readonly' => true, +- 'context' => array( 'view', 'edit' ), +- ), +- ), +- ); +- +- return $this->add_additional_fields_schema( $this->schema ); +- } +- +- /** +- * Retrieves the query params for the collections. +- * +- * @since 5.5.0 +- * +- * @return array Query parameters for the collection. +- */ +- public function get_collection_params() { +- $query_params = parent::get_collection_params(); +- +- $query_params['context']['default'] = 'view'; +- +- $query_params['status'] = array( +- 'description' => __( 'Limits results to plugins with the given status.', 'gutenberg' ), +- 'type' => 'array', +- 'items' => array( +- 'type' => 'string', +- 'enum' => is_multisite() ? array( 'inactive', 'active', 'network-active' ) : array( 'inactive', 'active' ), +- ), +- ); +- +- unset( $query_params['page'], $query_params['per_page'] ); +- +- return $query_params; +- } +-} +Skip. Widgets endpoint still experimental. +diff --git a/lib/class-wp-rest-widget-types-controller.php b/lib/class-wp-rest-widget-types-controller.php +index e39bc84f76..cf19a27331 100644 +--- a/lib/class-wp-rest-widget-types-controller.php ++++ b/lib/class-wp-rest-widget-types-controller.php +@@ -197,7 +197,7 @@ class WP_REST_Widget_Types_Controller extends WP_REST_Controller { + global $wp_registered_widgets; + + $widgets = array(); +- foreach ( $wp_registered_widgets as $slug => $widget ) { ++ foreach ( $wp_registered_widgets as $widget ) { + $widget_callback = $widget['callback']; + unset( $widget['callback'] ); + +Skip. FSE still experimental. +diff --git a/lib/class-wp-theme-json-resolver.php b/lib/class-wp-theme-json-resolver.php +new file mode 100644 +index 0000000000..67f5f7e12c +--- /dev/null ++++ b/lib/class-wp-theme-json-resolver.php +@@ -0,0 +1,435 @@ ++ $partial_child ) { ++ if ( is_numeric( $property ) ) { ++ return array( ++ array( ++ 'path' => $current_path, ++ 'translatable_keys' => $file_structure_partial, ++ ), ++ ); ++ } ++ $result = array_merge( ++ $result, ++ self::theme_json_i18_file_structure_to_preset_paths( $partial_child, array_merge( $current_path, array( $property ) ) ) ++ ); ++ } ++ return $result; ++ } ++ ++ /** ++ * Returns a data structure used in theme.json translation. ++ * ++ * @return array An array of theme.json paths that are translatable and the keys that are translatable ++ */ ++ private static function get_presets_to_translate() { ++ static $theme_json_i18n = null; ++ if ( null === $theme_json_i18n ) { ++ $file_structure = self::get_from_file( __DIR__ . '/experimental-i18n-theme.json' ); ++ $theme_json_i18n = self::theme_json_i18_file_structure_to_preset_paths( $file_structure ); ++ ++ } ++ return $theme_json_i18n; ++ } ++ ++ /** ++ * Translates a theme.json structure. ++ * ++ * @param array $theme_json_structure A theme.json structure that is going to be translatable. ++ * @param string $domain Optional. Text domain. Unique identifier for retrieving translated strings. ++ * Default 'default'. ++ */ ++ private static function translate_presets( &$theme_json_structure, $domain = 'default' ) { ++ $preset_to_translate = self::get_presets_to_translate(); ++ foreach ( $theme_json_structure as &$context_value ) { ++ if ( empty( $context_value ) ) { ++ continue; ++ } ++ foreach ( $preset_to_translate as $preset ) { ++ $path = $preset['path']; ++ $translatable_keys = $preset['translatable_keys']; ++ $array_to_translate = gutenberg_experimental_get( $context_value, $path, null ); ++ if ( null === $array_to_translate ) { ++ continue; ++ } ++ foreach ( $array_to_translate as &$item_to_translate ) { ++ foreach ( $translatable_keys as $translatable_key ) { ++ if ( empty( $item_to_translate[ $translatable_key ] ) ) { ++ continue; ++ } ++ // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain ++ $item_to_translate[ $translatable_key ] = translate( $item_to_translate[ $translatable_key ], $domain ); ++ // phpcs:enable ++ } ++ } ++ gutenberg_experimental_set( $context_value, $path, $array_to_translate ); ++ } ++ } ++ } ++ ++ /** ++ * Return core's origin config. ++ * ++ * @return WP_Theme_JSON Entity that holds core data. ++ */ ++ private static function get_core_origin() { ++ if ( null !== self::$core ) { ++ return self::$core; ++ } ++ ++ $config = self::get_from_file( __DIR__ . '/experimental-default-theme.json' ); ++ self::translate_presets( $config ); ++ ++ // Start i18n logic to remove when JSON i18 strings are extracted. ++ $default_colors_i18n = array( ++ 'black' => __( 'Black', 'gutenberg' ), ++ 'cyan-bluish-gray' => __( 'Cyan bluish gray', 'gutenberg' ), ++ 'white' => __( 'White', 'gutenberg' ), ++ 'pale-pink' => __( 'Pale pink', 'gutenberg' ), ++ 'vivid-red' => __( 'Vivid red', 'gutenberg' ), ++ 'luminous-vivid-orange' => __( 'Luminous vivid orange', 'gutenberg' ), ++ 'luminous-vivid-amber' => __( 'Luminous vivid amber', 'gutenberg' ), ++ 'light-green-cyan' => __( 'Light green cyan', 'gutenberg' ), ++ 'vivid-green-cyan' => __( 'Vivid green cyan', 'gutenberg' ), ++ 'pale-cyan-blue' => __( 'Pale cyan blue', 'gutenberg' ), ++ 'vivid-cyan-blue' => __( 'Vivid cyan blue', 'gutenberg' ), ++ 'vivid-purple' => __( 'Vivid purple', 'gutenberg' ), ++ ); ++ if ( ! empty( $config['global']['settings']['color']['palette'] ) ) { ++ foreach ( $config['global']['settings']['color']['palette'] as &$color ) { ++ $color['name'] = $default_colors_i18n[ $color['slug'] ]; ++ } ++ } ++ ++ $default_gradients_i18n = array( ++ 'vivid-cyan-blue-to-vivid-purple' => __( 'Vivid cyan blue to vivid purple', 'gutenberg' ), ++ 'light-green-cyan-to-vivid-green-cyan' => __( 'Light green cyan to vivid green cyan', 'gutenberg' ), ++ 'luminous-vivid-amber-to-luminous-vivid-orange' => __( 'Luminous vivid amber to luminous vivid orange', 'gutenberg' ), ++ 'luminous-vivid-orange-to-vivid-red' => __( 'Luminous vivid orange to vivid red', 'gutenberg' ), ++ 'very-light-gray-to-cyan-bluish-gray' => __( 'Very light gray to cyan bluish gray', 'gutenberg' ), ++ 'cool-to-warm-spectrum' => __( 'Cool to warm spectrum', 'gutenberg' ), ++ 'blush-light-purple' => __( 'Blush light purple', 'gutenberg' ), ++ 'blush-bordeaux' => __( 'Blush bordeaux', 'gutenberg' ), ++ 'luminous-dusk' => __( 'Luminous dusk', 'gutenberg' ), ++ 'pale-ocean' => __( 'Pale ocean', 'gutenberg' ), ++ 'electric-grass' => __( 'Electric grass', 'gutenberg' ), ++ 'midnight' => __( 'Midnight', 'gutenberg' ), ++ ); ++ if ( ! empty( $config['global']['settings']['color']['gradients'] ) ) { ++ foreach ( $config['global']['settings']['color']['gradients'] as &$gradient ) { ++ $gradient['name'] = $default_gradients_i18n[ $gradient['slug'] ]; ++ } ++ } ++ ++ $default_font_sizes_i18n = array( ++ 'small' => __( 'Small', 'gutenberg' ), ++ 'normal' => __( 'Normal', 'gutenberg' ), ++ 'medium' => __( 'Medium', 'gutenberg' ), ++ 'large' => __( 'Large', 'gutenberg' ), ++ 'huge' => __( 'Huge', 'gutenberg' ), ++ ); ++ if ( ! empty( $config['global']['settings']['typography']['fontSizes'] ) ) { ++ foreach ( $config['global']['settings']['typography']['fontSizes'] as &$font_size ) { ++ $font_size['name'] = $default_font_sizes_i18n[ $font_size['slug'] ]; ++ } ++ } ++ // End i18n logic to remove when JSON i18 strings are extracted. ++ ++ self::$core = new WP_Theme_JSON( $config ); ++ ++ return self::$core; ++ } ++ ++ /** ++ * Returns the theme's origin config. ++ * ++ * It uses the theme support data if ++ * the theme hasn't declared any via theme.json. ++ * ++ * @param array $theme_support_data Theme support data in theme.json format. ++ * ++ * @return WP_Theme_JSON Entity that holds theme data. ++ */ ++ private function get_theme_origin( $theme_support_data = array() ) { ++ $theme_json_data = self::get_from_file( locate_template( 'experimental-theme.json' ) ); ++ self::translate_presets( $theme_json_data, wp_get_theme()->get( 'TextDomain' ) ); ++ ++ /* ++ * We want the presets and settings declared in theme.json ++ * to override the ones declared via add_theme_support. ++ */ ++ $this->theme = new WP_Theme_JSON( $theme_support_data ); ++ $this->theme->merge( new WP_Theme_JSON( $theme_json_data ) ); ++ ++ return $this->theme; ++ } ++ ++ /** ++ * Returns the CPT that contains the user's origin config ++ * for the current theme or a void array if none found. ++ * ++ * It can also create and return a new draft CPT. ++ * ++ * @param bool $should_create_cpt Whether a new CPT should be created if no one was found. ++ * False by default. ++ * @param array $post_status_filter Filter CPT by post status. ++ * ['publish'] by default, so it only fetches published posts. ++ * ++ * @return array Custom Post Type for the user's origin config. ++ */ ++ private static function get_user_data_from_custom_post_type( $should_create_cpt = false, $post_status_filter = array( 'publish' ) ) { ++ $user_cpt = array(); ++ $post_type_filter = 'wp_global_styles'; ++ $post_name_filter = 'wp-global-styles-' . urlencode( wp_get_theme()->get_stylesheet() ); ++ $recent_posts = wp_get_recent_posts( ++ array( ++ 'numberposts' => 1, ++ 'orderby' => 'date', ++ 'order' => 'desc', ++ 'post_type' => $post_type_filter, ++ 'post_status' => $post_status_filter, ++ 'name' => $post_name_filter, ++ ) ++ ); ++ ++ if ( is_array( $recent_posts ) && ( count( $recent_posts ) === 1 ) ) { ++ $user_cpt = $recent_posts[0]; ++ } elseif ( $should_create_cpt ) { ++ $cpt_post_id = wp_insert_post( ++ array( ++ 'post_content' => '{}', ++ 'post_status' => 'publish', ++ 'post_type' => $post_type_filter, ++ 'post_name' => $post_name_filter, ++ ), ++ true ++ ); ++ $user_cpt = get_post( $cpt_post_id, ARRAY_A ); ++ } ++ ++ return $user_cpt; ++ } ++ ++ /** ++ * Returns the user's origin config. ++ * ++ * @return WP_Theme_JSON Entity that holds user data. ++ */ ++ private static function get_user_origin() { ++ if ( null !== self::$user ) { ++ return self::$user; ++ } ++ ++ $config = array(); ++ $user_cpt = self::get_user_data_from_custom_post_type(); ++ if ( array_key_exists( 'post_content', $user_cpt ) ) { ++ $decoded_data = json_decode( $user_cpt['post_content'], true ); ++ ++ $json_decoding_error = json_last_error(); ++ if ( JSON_ERROR_NONE !== $json_decoding_error ) { ++ error_log( 'Error when decoding user schema: ' . json_last_error_msg() ); ++ return $config; ++ } ++ ++ // Very important to verify if the flag isGlobalStylesUserThemeJSON is true. ++ // If is not true the content was not escaped and is not safe. ++ if ( ++ is_array( $decoded_data ) && ++ isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) && ++ $decoded_data['isGlobalStylesUserThemeJSON'] ++ ) { ++ unset( $decoded_data['isGlobalStylesUserThemeJSON'] ); ++ $config = $decoded_data; ++ } ++ } ++ self::$user = new WP_Theme_JSON( $config, true ); ++ ++ return self::$user; ++ } ++ ++ /** ++ * There are three sources of data for a site: ++ * core, theme, and user. ++ * ++ * The main function of the resolver is to ++ * merge all this data following this algorithm: ++ * theme overrides core, and user overrides ++ * data coming from either theme or core. ++ * ++ * user data > theme data > core data ++ * ++ * The main use case for the resolver is to return ++ * the merged data up to the user level.However, ++ * there are situations in which we need the ++ * data merged up to a different level (theme) ++ * or no merged at all. ++ * ++ * @param array $theme_support_data Existing block editor settings. ++ * Empty array by default. ++ * @param string $origin The source of data the consumer wants. ++ * Valid values are 'core', 'theme', 'user'. ++ * Default is 'user'. ++ * @param boolean $merged Whether the data should be merged ++ * with the previous origins (the default). ++ * ++ * @return WP_Theme_JSON ++ */ ++ public function get_origin( $theme_support_data = array(), $origin = 'user', $merged = true ) { ++ if ( ( 'user' === $origin ) && $merged ) { ++ $result = new WP_Theme_JSON(); ++ $result->merge( self::get_core_origin() ); ++ $result->merge( $this->get_theme_origin( $theme_support_data ) ); ++ $result->merge( self::get_user_origin() ); ++ return $result; ++ } ++ ++ if ( ( 'theme' === $origin ) && $merged ) { ++ $result = new WP_Theme_JSON(); ++ $result->merge( self::get_core_origin() ); ++ $result->merge( $this->get_theme_origin( $theme_support_data ) ); ++ return $result; ++ } ++ ++ if ( 'user' === $origin ) { ++ return self::get_user_origin(); ++ } ++ ++ if ( 'theme' === $origin ) { ++ return $this->get_theme_origin( $theme_support_data ); ++ } ++ ++ return self::get_core_origin(); ++ } ++ ++ /** ++ * Registers a Custom Post Type to store the user's origin config. ++ */ ++ public static function register_user_custom_post_type() { ++ $args = array( ++ 'label' => __( 'Global Styles', 'gutenberg' ), ++ 'description' => 'CPT to store user design tokens', ++ 'public' => false, ++ 'show_ui' => false, ++ 'show_in_rest' => true, ++ 'rest_base' => '__experimental/global-styles', ++ 'capabilities' => array( ++ 'read' => 'edit_theme_options', ++ 'create_posts' => 'edit_theme_options', ++ 'edit_posts' => 'edit_theme_options', ++ 'edit_published_posts' => 'edit_theme_options', ++ 'delete_published_posts' => 'edit_theme_options', ++ 'edit_others_posts' => 'edit_theme_options', ++ 'delete_others_posts' => 'edit_theme_options', ++ ), ++ 'map_meta_cap' => true, ++ 'supports' => array( ++ 'editor', ++ 'revisions', ++ ), ++ ); ++ register_post_type( 'wp_global_styles', $args ); ++ } ++ ++ /** ++ * Returns the ID of the custom post type ++ * that stores user data. ++ * ++ * @return integer ++ */ ++ public static function get_user_custom_post_type_id() { ++ if ( null !== self::$user_custom_post_type_id ) { ++ return self::$user_custom_post_type_id; ++ } ++ ++ $user_cpt = self::get_user_data_from_custom_post_type( true ); ++ if ( array_key_exists( 'ID', $user_cpt ) ) { ++ self::$user_custom_post_type_id = $user_cpt['ID']; ++ } ++ ++ return self::$user_custom_post_type_id; ++ } ++ ++} +Skip. FSE still experimental. +diff --git a/lib/class-wp-theme-json.php b/lib/class-wp-theme-json.php +new file mode 100644 +index 0000000000..c8cd12f410 +--- /dev/null ++++ b/lib/class-wp-theme-json.php +@@ -0,0 +1,1180 @@ ++ array( ++ 'border' => array( ++ 'radius' => null, ++ ), ++ 'color' => array( ++ 'background' => null, ++ 'gradient' => null, ++ 'link' => null, ++ 'text' => null, ++ ), ++ 'spacing' => array( ++ 'padding' => array( ++ 'top' => null, ++ 'right' => null, ++ 'bottom' => null, ++ 'left' => null, ++ ), ++ ), ++ 'typography' => array( ++ 'fontFamily' => null, ++ 'fontSize' => null, ++ 'fontStyle' => null, ++ 'fontWeight' => null, ++ 'lineHeight' => null, ++ 'textDecoration' => null, ++ 'textTransform' => null, ++ ), ++ ), ++ 'settings' => array( ++ 'border' => array( ++ 'customRadius' => null, ++ ), ++ 'color' => array( ++ 'custom' => null, ++ 'customGradient' => null, ++ 'gradients' => null, ++ 'link' => null, ++ 'palette' => null, ++ ), ++ 'spacing' => array( ++ 'customPadding' => null, ++ 'units' => null, ++ ), ++ 'typography' => array( ++ 'customFontSize' => null, ++ 'customLineHeight' => null, ++ 'dropCap' => null, ++ 'fontFamilies' => null, ++ 'fontSizes' => null, ++ 'customFontStyle' => null, ++ 'customFontWeight' => null, ++ 'customTextDecorations' => null, ++ 'customTextTransforms' => null, ++ ), ++ 'custom' => null, ++ ), ++ ); ++ ++ /** ++ * Presets are a set of values that serve ++ * to bootstrap some styles: colors, font sizes, etc. ++ * ++ * They are a unkeyed array of values such as: ++ * ++ * ```php ++ * array( ++ * array( ++ * 'slug' => 'unique-name-within-the-set', ++ * 'name' => 'Name for the UI', ++ * => 'value' ++ * ), ++ * ) ++ * ``` ++ * ++ * This contains the necessary metadata to process them: ++ * ++ * - path => where to find the preset in a theme.json context ++ * ++ * - value_key => the key that represents the value ++ * ++ * - css_var_infix => infix to use in generating the CSS Custom Property. Example: ++ * --wp--preset----: ++ * ++ * - classes => array containing a structure with the classes to ++ * generate for the presets. Each class should have ++ * the class suffix and the property name. Example: ++ * ++ * .has-- { ++ * : ++ * } ++ */ ++ const PRESETS_METADATA = array( ++ array( ++ 'path' => array( 'settings', 'color', 'palette' ), ++ 'value_key' => 'color', ++ 'css_var_infix' => 'color', ++ 'classes' => array( ++ array( ++ 'class_suffix' => 'color', ++ 'property_name' => 'color', ++ ), ++ array( ++ 'class_suffix' => 'background-color', ++ 'property_name' => 'background-color', ++ ), ++ ), ++ ), ++ array( ++ 'path' => array( 'settings', 'color', 'gradients' ), ++ 'value_key' => 'gradient', ++ 'css_var_infix' => 'gradient', ++ 'classes' => array( ++ array( ++ 'class_suffix' => 'gradient-background', ++ 'property_name' => 'background', ++ ), ++ ), ++ ), ++ array( ++ 'path' => array( 'settings', 'typography', 'fontSizes' ), ++ 'value_key' => 'size', ++ 'css_var_infix' => 'font-size', ++ 'classes' => array( ++ array( ++ 'class_suffix' => 'font-size', ++ 'property_name' => 'font-size', ++ ), ++ ), ++ ), ++ array( ++ 'path' => array( 'settings', 'typography', 'fontFamilies' ), ++ 'value_key' => 'fontFamily', ++ 'css_var_infix' => 'font-family', ++ 'classes' => array(), ++ ), ++ ); ++ ++ /** ++ * Metadata for style properties. ++ * ++ * Each property declares: ++ * ++ * - 'value': path to the value in theme.json and block attributes. ++ * - 'support': path to the block support in block.json. ++ */ ++ const PROPERTIES_METADATA = array( ++ '--wp--style--color--link' => array( ++ 'value' => array( 'color', 'link' ), ++ 'support' => array( 'color', 'link' ), ++ ), ++ 'background' => array( ++ 'value' => array( 'color', 'gradient' ), ++ 'support' => array( 'color', 'gradients' ), ++ ), ++ 'backgroundColor' => array( ++ 'value' => array( 'color', 'background' ), ++ 'support' => array( 'color' ), ++ ), ++ 'borderRadius' => array( ++ 'value' => array( 'border', 'radius' ), ++ 'support' => array( '__experimentalBorder', 'radius' ), ++ ), ++ 'color' => array( ++ 'value' => array( 'color', 'text' ), ++ 'support' => array( 'color' ), ++ ), ++ 'fontFamily' => array( ++ 'value' => array( 'typography', 'fontFamily' ), ++ 'support' => array( '__experimentalFontFamily' ), ++ ), ++ 'fontSize' => array( ++ 'value' => array( 'typography', 'fontSize' ), ++ 'support' => array( 'fontSize' ), ++ ), ++ 'fontStyle' => array( ++ 'value' => array( 'typography', 'fontStyle' ), ++ 'support' => array( '__experimentalFontStyle' ), ++ ), ++ 'fontWeight' => array( ++ 'value' => array( 'typography', 'fontWeight' ), ++ 'support' => array( '__experimentalFontWeight' ), ++ ), ++ 'lineHeight' => array( ++ 'value' => array( 'typography', 'lineHeight' ), ++ 'support' => array( 'lineHeight' ), ++ ), ++ 'padding' => array( ++ 'value' => array( 'spacing', 'padding' ), ++ 'support' => array( 'spacing', 'padding' ), ++ 'properties' => array( 'top', 'right', 'bottom', 'left' ), ++ ), ++ 'textDecoration' => array( ++ 'value' => array( 'typography', 'textDecoration' ), ++ 'support' => array( '__experimentalTextDecoration' ), ++ ), ++ 'textTransform' => array( ++ 'value' => array( 'typography', 'textTransform' ), ++ 'support' => array( '__experimentalTextTransform' ), ++ ), ++ ); ++ ++ /** ++ * Constructor. ++ * ++ * @param array $contexts A structure that follows the theme.json schema. ++ * @param boolean $should_escape_styles Whether the incoming styles should be escaped. ++ */ ++ public function __construct( $contexts = array(), $should_escape_styles = false ) { ++ $this->contexts = array(); ++ ++ if ( ! is_array( $contexts ) ) { ++ return; ++ } ++ ++ $metadata = $this->get_blocks_metadata(); ++ foreach ( $contexts as $key => $context ) { ++ if ( ! isset( $metadata[ $key ] ) ) { ++ // Skip incoming contexts that can't be found ++ // within the contexts registered. ++ continue; ++ } ++ ++ // Filter out top-level keys that aren't valid according to the schema. ++ $context = array_intersect_key( $context, self::SCHEMA ); ++ ++ // Process styles subtree. ++ $this->process_key( 'styles', $context, self::SCHEMA ); ++ if ( isset( $context['styles'] ) ) { ++ $this->process_key( 'border', $context['styles'], self::SCHEMA['styles'], $should_escape_styles ); ++ $this->process_key( 'color', $context['styles'], self::SCHEMA['styles'], $should_escape_styles ); ++ $this->process_key( 'spacing', $context['styles'], self::SCHEMA['styles'], $should_escape_styles ); ++ $this->process_key( 'typography', $context['styles'], self::SCHEMA['styles'], $should_escape_styles ); ++ ++ if ( empty( $context['styles'] ) ) { ++ unset( $context['styles'] ); ++ } else { ++ $this->contexts[ $key ]['styles'] = $context['styles']; ++ } ++ } ++ ++ // Process settings subtree. ++ $this->process_key( 'settings', $context, self::SCHEMA ); ++ if ( isset( $context['settings'] ) ) { ++ $this->process_key( 'border', $context['settings'], self::SCHEMA['settings'] ); ++ $this->process_key( 'color', $context['settings'], self::SCHEMA['settings'] ); ++ $this->process_key( 'spacing', $context['settings'], self::SCHEMA['settings'] ); ++ $this->process_key( 'typography', $context['settings'], self::SCHEMA['settings'] ); ++ ++ if ( empty( $context['settings'] ) ) { ++ unset( $context['settings'] ); ++ } else { ++ $this->contexts[ $key ]['settings'] = $context['settings']; ++ } ++ } ++ } ++ } ++ ++ /** ++ * Returns a mapping on metadata properties to avoid having to constantly ++ * transforms properties between camel case and kebab. ++ * ++ * @return array Containing three mappings ++ * "to_kebab_case" mapping properties in camel case to ++ * properties in kebab case e.g: "paddingTop" to "padding-top". ++ * "to_camel_case" mapping properties in kebab case to ++ * properties in camel case e.g: "padding-top" to "paddingTop". ++ * "to_property" mapping properties in kebab case to ++ * the main properties in camel case e.g: "padding-top" to "padding". ++ */ ++ private static function get_properties_metadata_case_mappings() { ++ static $properties_metadata_case_mappings; ++ if ( null === $properties_metadata_case_mappings ) { ++ $properties_metadata_case_mappings = array( ++ 'to_kebab_case' => array(), ++ 'to_camel_case' => array(), ++ 'to_property' => array(), ++ ); ++ foreach ( self::PROPERTIES_METADATA as $key => $metadata ) { ++ $kebab_case = strtolower( preg_replace( '/(? array( ++ 'selector' => self::GLOBAL_SELECTOR, ++ 'supports' => self::GLOBAL_SUPPORTS, ++ ), ++ ); ++ ++ $registry = WP_Block_Type_Registry::get_instance(); ++ $blocks = $registry->get_all_registered(); ++ foreach ( $blocks as $block_name => $block_type ) { ++ /* ++ * Skips blocks that don't declare support, ++ * they don't generate styles. ++ */ ++ if ( ++ ! property_exists( $block_type, 'supports' ) || ++ ! is_array( $block_type->supports ) || ++ empty( $block_type->supports ) ++ ) { ++ continue; ++ } ++ ++ /* ++ * Extract block support keys that are related to the style properties. ++ */ ++ $block_supports = array(); ++ foreach ( self::PROPERTIES_METADATA as $key => $metadata ) { ++ if ( gutenberg_experimental_get( $block_type->supports, $metadata['support'] ) ) { ++ $block_supports[] = $key; ++ } ++ } ++ ++ /* ++ * Skip blocks that don't support anything related to styles. ++ */ ++ if ( empty( $block_supports ) ) { ++ continue; ++ } ++ ++ /* ++ * Assign the selector for the block. ++ * ++ * Some blocks can declare multiple selectors: ++ * ++ * - core/heading represents the H1-H6 HTML elements ++ * - core/list represents the UL and OL HTML elements ++ * - core/group is meant to represent DIV and other HTML elements ++ * ++ * Some other blocks don't provide a selector, ++ * so we generate a class for them based on their name: ++ * ++ * - 'core/group' => '.wp-block-group' ++ * - 'my-custom-library/block-name' => '.wp-block-my-custom-library-block-name' ++ * ++ * Note that, for core blocks, we don't add the `core/` prefix to its class name. ++ * This is for historical reasons, as they come with a class without that infix. ++ * ++ */ ++ if ( ++ isset( $block_type->supports['__experimentalSelector'] ) && ++ is_string( $block_type->supports['__experimentalSelector'] ) ++ ) { ++ self::$blocks_metadata[ $block_name ] = array( ++ 'selector' => $block_type->supports['__experimentalSelector'], ++ 'supports' => $block_supports, ++ ); ++ } elseif ( ++ isset( $block_type->supports['__experimentalSelector'] ) && ++ is_array( $block_type->supports['__experimentalSelector'] ) ++ ) { ++ foreach ( $block_type->supports['__experimentalSelector'] as $key => $selector_metadata ) { ++ if ( ! isset( $selector_metadata['selector'] ) ) { ++ continue; ++ } ++ ++ self::$blocks_metadata[ $key ] = array( ++ 'selector' => $selector_metadata['selector'], ++ 'supports' => $block_supports, ++ ); ++ } ++ } else { ++ self::$blocks_metadata[ $block_name ] = array( ++ 'selector' => '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) ), ++ 'supports' => $block_supports, ++ ); ++ } ++ } ++ ++ return self::$blocks_metadata; ++ } ++ ++ /** ++ * Normalize the subtree according to the given schema. ++ * This function modifies the given input by removing ++ * the nodes that aren't valid per the schema. ++ * ++ * @param string $key Key of the subtree to normalize. ++ * @param array $input Whole tree to normalize. ++ * @param array $schema Schema to use for normalization. ++ * @param boolean $should_escape Whether the subproperties should be escaped. ++ */ ++ private static function process_key( $key, &$input, $schema, $should_escape = false ) { ++ if ( ! isset( $input[ $key ] ) ) { ++ return; ++ } ++ ++ // Consider valid the input value. ++ if ( null === $schema[ $key ] ) { ++ return; ++ } ++ ++ if ( ! is_array( $input[ $key ] ) ) { ++ unset( $input[ $key ] ); ++ return; ++ } ++ ++ $input[ $key ] = array_intersect_key( ++ $input[ $key ], ++ $schema[ $key ] ++ ); ++ ++ if ( $should_escape ) { ++ $subtree = $input[ $key ]; ++ foreach ( $subtree as $property => $value ) { ++ $name = 'background-color'; ++ if ( 'gradient' === $property ) { ++ $name = 'background'; ++ } ++ ++ if ( is_array( $value ) ) { ++ $result = array(); ++ foreach ( $value as $subproperty => $subvalue ) { ++ $result_subproperty = safecss_filter_attr( "$name: $subvalue" ); ++ if ( '' !== $result_subproperty ) { ++ $result[ $subproperty ] = $result_subproperty; ++ } ++ } ++ ++ if ( empty( $result ) ) { ++ unset( $input[ $key ][ $property ] ); ++ } ++ } else { ++ $result = safecss_filter_attr( "$name: $value" ); ++ ++ if ( '' === $result ) { ++ unset( $input[ $key ][ $property ] ); ++ } ++ } ++ } ++ } ++ ++ if ( 0 === count( $input[ $key ] ) ) { ++ unset( $input[ $key ] ); ++ } ++ } ++ ++ /** ++ * Given a context, it returns its settings subtree. ++ * ++ * @param array $context Context adhering to the theme.json schema. ++ * ++ * @return array|null The settings subtree. ++ */ ++ private static function extract_settings( $context ) { ++ if ( empty( $context['settings'] ) ) { ++ return null; ++ } ++ ++ return $context['settings']; ++ } ++ ++ /** ++ * Given a tree, it creates a flattened one ++ * by merging the keys and binding the leaf values ++ * to the new keys. ++ * ++ * It also transforms camelCase names into kebab-case ++ * and substitutes '/' by '-'. ++ * ++ * This is thought to be useful to generate ++ * CSS Custom Properties from a tree, ++ * although there's nothing in the implementation ++ * of this function that requires that format. ++ * ++ * For example, assuming the given prefix is '--wp' ++ * and the token is '--', for this input tree: ++ * ++ * { ++ * 'some/property': 'value', ++ * 'nestedProperty': { ++ * 'sub-property': 'value' ++ * } ++ * } ++ * ++ * it'll return this output: ++ * ++ * { ++ * '--wp--some-property': 'value', ++ * '--wp--nested-property--sub-property': 'value' ++ * } ++ * ++ * @param array $tree Input tree to process. ++ * @param string $prefix Prefix to prepend to each variable. '' by default. ++ * @param string $token Token to use between levels. '--' by default. ++ * ++ * @return array The flattened tree. ++ */ ++ private static function flatten_tree( $tree, $prefix = '', $token = '--' ) { ++ $result = array(); ++ foreach ( $tree as $property => $value ) { ++ $new_key = $prefix . str_replace( ++ '/', ++ '-', ++ strtolower( preg_replace( '/(? 'property_name', ++ * 'value' => 'property_value, ++ * ) ++ * ``` ++ * ++ * Note that this modifies the $declarations in place. ++ * ++ * @param array $declarations Holds the existing declarations. ++ * @param array $context Input context to process. ++ * @param array $context_supports Supports information for this context. ++ */ ++ private static function compute_style_properties( &$declarations, $context, $context_supports ) { ++ if ( empty( $context['styles'] ) ) { ++ return; ++ } ++ $metadata_mappings = self::get_properties_metadata_case_mappings(); ++ $properties = array(); ++ foreach ( self::PROPERTIES_METADATA as $name => $metadata ) { ++ if ( ! in_array( $name, $context_supports, true ) ) { ++ continue; ++ } ++ ++ // Some properties can be shorthand properties, meaning that ++ // they contain multiple values instead of a single one. ++ if ( self::has_properties( $metadata ) ) { ++ foreach ( $metadata['properties'] as $property ) { ++ $properties[] = array( ++ 'name' => $name . ucfirst( $property ), ++ 'value' => array_merge( $metadata['value'], array( $property ) ), ++ ); ++ } ++ } else { ++ $properties[] = array( ++ 'name' => $name, ++ 'value' => $metadata['value'], ++ ); ++ } ++ } ++ ++ foreach ( $properties as $prop ) { ++ $value = self::get_property_value( $context['styles'], $prop['value'] ); ++ if ( ! empty( $value ) ) { ++ $kebab_cased_name = $metadata_mappings['to_kebab_case'][ $prop['name'] ]; ++ $declarations[] = array( ++ 'name' => $kebab_cased_name, ++ 'value' => $value, ++ ); ++ } ++ } ++ } ++ ++ /** ++ * Given a context, it extracts its presets ++ * and adds them to the given input $stylesheet. ++ * ++ * Note this function modifies $stylesheet in place. ++ * ++ * @param string $stylesheet Input stylesheet to add the presets to. ++ * @param array $context Context to process. ++ * @param string $selector Selector wrapping the classes. ++ */ ++ private static function compute_preset_classes( &$stylesheet, $context, $selector ) { ++ if ( self::GLOBAL_SELECTOR === $selector ) { ++ // Classes at the global level do not need any CSS prefixed, ++ // and we don't want to increase its specificity. ++ $selector = ''; ++ } ++ ++ foreach ( self::PRESETS_METADATA as $preset ) { ++ $values = gutenberg_experimental_get( $context, $preset['path'], array() ); ++ foreach ( $values as $value ) { ++ foreach ( $preset['classes'] as $class ) { ++ $stylesheet .= self::to_ruleset( ++ $selector . '.has-' . $value['slug'] . '-' . $class['class_suffix'], ++ array( ++ array( ++ 'name' => $class['property_name'], ++ 'value' => $value[ $preset['value_key'] ], ++ ), ++ ) ++ ); ++ } ++ } ++ } ++ } ++ ++ /** ++ * Given a context, it extracts the CSS Custom Properties ++ * for the presets and adds them to the $declarations array ++ * following the format: ++ * ++ * ```php ++ * array( ++ * 'name' => 'property_name', ++ * 'value' => 'property_value, ++ * ) ++ * ``` ++ * ++ * Note that this modifies the $declarations in place. ++ * ++ * @param array $declarations Holds the existing declarations. ++ * @param array $context Input context to process. ++ */ ++ private static function compute_preset_vars( &$declarations, $context ) { ++ foreach ( self::PRESETS_METADATA as $preset ) { ++ $values = gutenberg_experimental_get( $context, $preset['path'], array() ); ++ foreach ( $values as $value ) { ++ $declarations[] = array( ++ 'name' => '--wp--preset--' . $preset['css_var_infix'] . '--' . $value['slug'], ++ 'value' => $value[ $preset['value_key'] ], ++ ); ++ } ++ } ++ } ++ ++ /** ++ * Given a context, it extracts the CSS Custom Properties ++ * for the custom values and adds them to the $declarations ++ * array following the format: ++ * ++ * ```php ++ * array( ++ * 'name' => 'property_name', ++ * 'value' => 'property_value, ++ * ) ++ * ``` ++ * ++ * Note that this modifies the $declarations in place. ++ * ++ * @param array $declarations Holds the existing declarations. ++ * @param array $context Input context to process. ++ */ ++ private static function compute_theme_vars( &$declarations, $context ) { ++ $custom_values = gutenberg_experimental_get( $context, array( 'settings', 'custom' ) ); ++ $css_vars = self::flatten_tree( $custom_values ); ++ foreach ( $css_vars as $key => $value ) { ++ $declarations[] = array( ++ 'name' => '--wp--custom--' . $key, ++ 'value' => $value, ++ ); ++ } ++ } ++ ++ /** ++ * Given a selector and a declaration list, ++ * creates the corresponding ruleset. ++ * ++ * To help debugging, will add some space ++ * if SCRIPT_DEBUG is defined and true. ++ * ++ * @param string $selector CSS selector. ++ * @param array $declarations List of declarations. ++ * ++ * @return string CSS ruleset. ++ */ ++ private static function to_ruleset( $selector, $declarations ) { ++ if ( empty( $declarations ) ) { ++ return ''; ++ } ++ $ruleset = ''; ++ ++ if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) { ++ $declaration_block = array_reduce( ++ $declarations, ++ function ( $carry, $element ) { ++ return $carry .= "\t" . $element['name'] . ': ' . $element['value'] . ";\n"; }, ++ '' ++ ); ++ $ruleset .= $selector . " {\n" . $declaration_block . "}\n"; ++ } else { ++ $declaration_block = array_reduce( ++ $declarations, ++ function ( $carry, $element ) { ++ return $carry .= $element['name'] . ': ' . $element['value'] . ';'; }, ++ '' ++ ); ++ $ruleset .= $selector . '{' . $declaration_block . '}'; ++ } ++ ++ return $ruleset; ++ } ++ ++ /** ++ * Converts each context into a list of rulesets ++ * to be appended to the stylesheet. ++ * These rulesets contain all the css variables (custom variables and preset variables). ++ * ++ * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax ++ * ++ * For each context this creates a new ruleset such as: ++ * ++ * context-selector { ++ * --wp--preset--category--slug: value; ++ * --wp--custom--variable: value; ++ * } ++ * ++ * @return string The new stylesheet. ++ */ ++ private function get_css_variables() { ++ $stylesheet = ''; ++ $metadata = $this->get_blocks_metadata(); ++ foreach ( $this->contexts as $context_name => $context ) { ++ if ( empty( $metadata[ $context_name ]['selector'] ) ) { ++ continue; ++ } ++ $selector = $metadata[ $context_name ]['selector']; ++ ++ $declarations = array(); ++ self::compute_preset_vars( $declarations, $context ); ++ self::compute_theme_vars( $declarations, $context ); ++ ++ // Attach the ruleset for style and custom properties. ++ $stylesheet .= self::to_ruleset( $selector, $declarations ); ++ } ++ return $stylesheet; ++ } ++ ++ /** ++ * Converts each context into a list of rulesets ++ * containing the block styles to be appended to the stylesheet. ++ * ++ * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax ++ * ++ * For each context this creates a new ruleset such as: ++ * ++ * context-selector { ++ * style-property-one: value; ++ * } ++ * ++ * Additionally, it'll also create new rulesets ++ * as classes for each preset value such as: ++ * ++ * .has-value-color { ++ * color: value; ++ * } ++ * ++ * .has-value-background-color { ++ * background-color: value; ++ * } ++ * ++ * .has-value-font-size { ++ * font-size: value; ++ * } ++ * ++ * .has-value-gradient-background { ++ * background: value; ++ * } ++ * ++ * p.has-value-gradient-background { ++ * background: value; ++ * } ++ * ++ * @return string The new stylesheet. ++ */ ++ private function get_block_styles() { ++ $stylesheet = ''; ++ $metadata = $this->get_blocks_metadata(); ++ foreach ( $this->contexts as $context_name => $context ) { ++ if ( empty( $metadata[ $context_name ]['selector'] ) || empty( $metadata[ $context_name ]['supports'] ) ) { ++ continue; ++ } ++ $selector = $metadata[ $context_name ]['selector']; ++ $supports = $metadata[ $context_name ]['supports']; ++ ++ $declarations = array(); ++ self::compute_style_properties( $declarations, $context, $supports ); ++ ++ $stylesheet .= self::to_ruleset( $selector, $declarations ); ++ ++ // Attach the rulesets for the classes. ++ self::compute_preset_classes( $stylesheet, $context, $selector ); ++ } ++ ++ return $stylesheet; ++ } ++ ++ /** ++ * Returns the existing settings for each context. ++ * ++ * Example: ++ * ++ * { ++ * 'global': { ++ * 'color': { ++ * 'custom': true ++ * } ++ * }, ++ * 'core/paragraph': { ++ * 'spacing': { ++ * 'customPadding': true ++ * } ++ * } ++ * } ++ * ++ * @return array Settings per context. ++ */ ++ public function get_settings() { ++ return array_filter( ++ array_map( array( $this, 'extract_settings' ), $this->contexts ), ++ function ( $element ) { ++ return null !== $element; ++ } ++ ); ++ } ++ ++ /** ++ * Returns the stylesheet that results of processing ++ * the theme.json structure this object represents. ++ * ++ * @param string $type Type of stylesheet we want accepts 'all', 'block_styles', and 'css_variables'. ++ * @return string Stylesheet. ++ */ ++ public function get_stylesheet( $type = 'all' ) { ++ switch ( $type ) { ++ case 'block_styles': ++ return $this->get_block_styles(); ++ case 'css_variables': ++ return $this->get_css_variables(); ++ default: ++ return $this->get_css_variables() . $this->get_block_styles(); ++ } ++ } ++ ++ /** ++ * Merge new incoming data. ++ * ++ * @param WP_Theme_JSON $theme_json Data to merge. ++ */ ++ public function merge( $theme_json ) { ++ $incoming_data = $theme_json->get_raw_data(); ++ ++ foreach ( array_keys( $incoming_data ) as $context ) { ++ foreach ( array( 'settings', 'styles' ) as $subtree ) { ++ if ( ! isset( $incoming_data[ $context ][ $subtree ] ) ) { ++ continue; ++ } ++ ++ if ( ! isset( $this->contexts[ $context ][ $subtree ] ) ) { ++ $this->contexts[ $context ][ $subtree ] = $incoming_data[ $context ][ $subtree ]; ++ continue; ++ } ++ ++ foreach ( array_keys( self::SCHEMA[ $subtree ] ) as $leaf ) { ++ if ( ! isset( $incoming_data[ $context ][ $subtree ][ $leaf ] ) ) { ++ continue; ++ } ++ ++ if ( ! isset( $this->contexts[ $context ][ $subtree ][ $leaf ] ) ) { ++ $this->contexts[ $context ][ $subtree ][ $leaf ] = $incoming_data[ $context ][ $subtree ][ $leaf ]; ++ continue; ++ } ++ ++ $this->contexts[ $context ][ $subtree ][ $leaf ] = array_merge( ++ $this->contexts[ $context ][ $subtree ][ $leaf ], ++ $incoming_data[ $context ][ $subtree ][ $leaf ] ++ ); ++ } ++ } ++ } ++ } ++ ++ /** ++ * Removes insecure data from theme.json. ++ */ ++ public function remove_insecure_properties() { ++ $blocks_metadata = self::get_blocks_metadata(); ++ $metadata_mappings = self::get_properties_metadata_case_mappings(); ++ foreach ( $this->contexts as $context_name => &$context ) { ++ // Escape the context key. ++ if ( empty( $blocks_metadata[ $context_name ] ) ) { ++ unset( $this->contexts[ $context_name ] ); ++ continue; ++ } ++ ++ $escaped_settings = null; ++ $escaped_styles = null; ++ ++ // Style escaping. ++ if ( ! empty( $context['styles'] ) ) { ++ $supports = $blocks_metadata[ $context_name ]['supports']; ++ $declarations = array(); ++ self::compute_style_properties( $declarations, $context, $supports ); ++ foreach ( $declarations as $declaration ) { ++ $style_to_validate = $declaration['name'] . ': ' . $declaration['value']; ++ if ( esc_html( safecss_filter_attr( $style_to_validate ) ) === $style_to_validate ) { ++ if ( null === $escaped_styles ) { ++ $escaped_styles = array(); ++ } ++ $property = $metadata_mappings['to_property'][ $declaration['name'] ]; ++ $path = self::PROPERTIES_METADATA[ $property ]['value']; ++ if ( self::has_properties( self::PROPERTIES_METADATA[ $property ] ) ) { ++ $declaration_divided = explode( '-', $declaration['name'] ); ++ $path[] = $declaration_divided[1]; ++ gutenberg_experimental_set( ++ $escaped_styles, ++ $path, ++ gutenberg_experimental_get( $context['styles'], $path ) ++ ); ++ } else { ++ gutenberg_experimental_set( ++ $escaped_styles, ++ $path, ++ gutenberg_experimental_get( $context['styles'], $path ) ++ ); ++ } ++ } ++ } ++ } ++ ++ // Settings escaping. ++ // For now the ony allowed settings are presets. ++ if ( ! empty( $context['settings'] ) ) { ++ foreach ( self::PRESETS_METADATA as $preset_metadata ) { ++ $current_preset = gutenberg_experimental_get( $context, $preset_metadata['path'], null ); ++ if ( null !== $current_preset ) { ++ $escaped_preset = array(); ++ foreach ( $current_preset as $single_preset ) { ++ if ( ++ esc_attr( esc_html( $single_preset['name'] ) ) === $single_preset['name'] && ++ sanitize_html_class( $single_preset['slug'] ) === $single_preset['slug'] ++ ) { ++ $value = $single_preset[ $preset_metadata['value_key'] ]; ++ $single_preset_is_valid = null; ++ if ( isset( $preset_metadata['classes'] ) && count( $preset_metadata['classes'] ) > 0 ) { ++ $single_preset_is_valid = true; ++ foreach ( $preset_metadata['classes'] as $class_meta_data ) { ++ $property = $class_meta_data['property_name']; ++ $style_to_validate = $property . ': ' . $value; ++ if ( esc_html( safecss_filter_attr( $style_to_validate ) ) !== $style_to_validate ) { ++ $single_preset_is_valid = false; ++ break; ++ } ++ } ++ } else { ++ $property = $preset_metadata['css_var_infix']; ++ $style_to_validate = $property . ': ' . $value; ++ $single_preset_is_valid = esc_html( safecss_filter_attr( $style_to_validate ) ) === $style_to_validate; ++ } ++ if ( $single_preset_is_valid ) { ++ $escaped_preset[] = $single_preset; ++ } ++ } ++ } ++ if ( count( $escaped_preset ) > 0 ) { ++ if ( null === $escaped_settings ) { ++ $escaped_settings = array(); ++ } ++ gutenberg_experimental_set( $escaped_settings, $preset_metadata['path'], $escaped_preset ); ++ } ++ } ++ } ++ if ( null !== $escaped_settings ) { ++ $escaped_settings = $escaped_settings['settings']; ++ } ++ } ++ ++ if ( null === $escaped_settings && null === $escaped_styles ) { ++ unset( $this->contexts[ $context_name ] ); ++ } elseif ( null !== $escaped_settings && null !== $escaped_styles ) { ++ $context = array( ++ 'styles' => $escaped_styles, ++ 'settings' => $escaped_settings, ++ ); ++ } elseif ( null === $escaped_settings ) { ++ $context = array( ++ 'styles' => $escaped_styles, ++ ); ++ } else { ++ $context = array( ++ 'settings' => $escaped_settings, ++ ); ++ } ++ } ++ } ++ ++ /** ++ * Retuns the raw data. ++ * ++ * @return array Raw data. ++ */ ++ public function get_raw_data() { ++ return $this->contexts; ++ } ++ ++} +Skip. Widgets still experimental. +diff --git a/lib/class-wp-widget-block.php b/lib/class-wp-widget-block.php +index 0e1bca7335..cc6ab5f174 100644 +--- a/lib/class-wp-widget-block.php ++++ b/lib/class-wp-widget-block.php +@@ -54,7 +54,17 @@ class WP_Widget_Block extends WP_Widget { + */ + public function widget( $args, $instance ) { + echo $args['before_widget']; +- echo do_blocks( $instance['content'] ); ++ $content = do_blocks( $instance['content'] ); ++ ++ // Handle embeds for block widgets. ++ // ++ // When this feature is added to core it may need to be implemented ++ // differently. WP_Widget_Text is a good reference, that applies a ++ // filter for its content, which WP_Embed uses in its constructor. ++ // See https://core.trac.wordpress.org/ticket/51566. ++ global $wp_embed; ++ echo $wp_embed->autoembed( $content ); ++ + echo $args['after_widget']; + } + +TODO Need to come back to client-assets.php.... +TODO. Not sure about the i18n change. Asked in https://github.com/WordPress/gutenberg/pull/28279. +TODO object-fit polyfill - that library needs to publish an unminified version. +Skipped should_load_seperate files as that's FSE related. +Skipped editor styles stuff as I think that's FSE related? Not 100%. +diff --git a/lib/client-assets.php b/lib/client-assets.php +index f1a72422ba..a8af2b2b34 100644 +--- a/lib/client-assets.php ++++ b/lib/client-assets.php +@@ -18,7 +18,7 @@ if ( ! defined( 'ABSPATH' ) ) { + * @since 0.1.0 + */ + function gutenberg_dir_path() { +- return plugin_dir_path( dirname( __FILE__ ) ); ++ return plugin_dir_path( __DIR__ ); + } + + /** +@@ -31,7 +31,7 @@ function gutenberg_dir_path() { + * @since 0.1.0 + */ + function gutenberg_url( $path ) { +- return plugins_url( $path, dirname( __FILE__ ) ); ++ return plugins_url( $path, __DIR__ ); + } + + /** +@@ -95,6 +95,11 @@ function gutenberg_override_script( $scripts, $handle, $src, $deps = array(), $v + if ( 'wp-i18n' !== $handle && 'wp-polyfill' !== $handle ) { + $scripts->set_translations( $handle, 'default' ); + } ++ if ( 'wp-i18n' === $handle ) { ++ $ltr = 'rtl' === _x( 'ltr', 'text direction', 'default' ) ? 'rtl' : 'ltr'; ++ $output = sprintf( "wp.i18n.setLocaleData( { 'text direction\u0004ltr': [ '%s' ] }, 'default' );", $ltr ); ++ $scripts->add_inline_script( 'wp-i18n', $output, 'after' ); ++ } + } + + /** +@@ -234,6 +239,14 @@ function gutenberg_register_vendor_scripts( $scripts ) { + '4.17.19', + true + ); ++ ++ gutenberg_register_vendor_script( ++ $scripts, ++ 'object-fit-polyfill', ++ 'https://unpkg.com/objectFitPolyfill@2.3.0/dist/objectFitPolyfill.min.js', ++ array(), ++ '2.3.0' ++ ); + } + add_action( 'wp_default_scripts', 'gutenberg_register_vendor_scripts' ); + +@@ -335,12 +348,13 @@ function gutenberg_register_packages_styles( $styles ) { + ); + $styles->add_data( 'wp-components', 'rtl', 'replace' ); + ++ $block_library_filename = gutenberg_should_load_separate_block_styles() ? 'common' : 'style'; + gutenberg_override_style( + $styles, + 'wp-block-library', +- gutenberg_url( 'build/block-library/style.css' ), ++ gutenberg_url( 'build/block-library/' . $block_library_filename . '.css' ), + array(), +- filemtime( gutenberg_dir_path() . 'build/block-library/style.css' ) ++ filemtime( gutenberg_dir_path() . 'build/block-library/' . $block_library_filename . '.css' ) + ); + $styles->add_data( 'wp-block-library', 'rtl', 'replace' ); + +@@ -526,15 +540,25 @@ function gutenberg_register_vendor_script( $scripts, $handle, $src, $deps = arra + // Determine whether we can write to this file. If not, don't waste + // time doing a network request. + // @codingStandardsIgnoreStart +- $f = @fopen( $full_path, 'a' ); ++ ++ $is_writable = is_writable( $full_path ); ++ if ( $is_writable ) { ++ $f = @fopen( $full_path, 'a' ); ++ if ( ! $f ) { ++ $is_writable = false; ++ } else { ++ fclose( $f ); ++ } ++ } ++ + // @codingStandardsIgnoreEnd +- if ( ! $f ) { ++ if ( ! $is_writable ) { + // Failed to open the file for writing, probably due to server + // permissions. Enqueue the script directly from the URL instead. + gutenberg_override_script( $scripts, $handle, $src, $deps, $ver, $in_footer ); + return; + } +- fclose( $f ); ++ + $response = wp_remote_get( $src ); + if ( wp_remote_retrieve_response_code( $response ) === 200 ) { + $f = fopen( $full_path, 'w' ); +@@ -568,7 +592,9 @@ function gutenberg_register_vendor_script( $scripts, $handle, $src, $deps = arra + * @return array Filtered editor settings. + */ + function gutenberg_extend_block_editor_styles( $settings ) { +- $editor_styles_file = gutenberg_dir_path() . 'build/editor/editor-styles.css'; ++ $editor_styles_file = is_rtl() ? ++ gutenberg_dir_path() . 'build/editor/editor-styles-rtl.css' : ++ gutenberg_dir_path() . 'build/editor/editor-styles.css'; + + /* + * If, for whatever reason, the built editor styles do not exist, avoid +@@ -590,7 +616,9 @@ function gutenberg_extend_block_editor_styles( $settings ) { + */ + + $default_styles = file_get_contents( +- ABSPATH . WPINC . '/css/dist/editor/editor-styles.css' ++ is_rtl() ? ++ ABSPATH . WPINC . '/css/dist/editor/editor-styles-rtl.css' : ++ ABSPATH . WPINC . '/css/dist/editor/editor-styles.css' + ); + + /* +@@ -629,7 +657,9 @@ add_filter( 'block_editor_settings', 'gutenberg_extend_block_editor_styles' ); + * @return array Filtered editor settings. + */ + function gutenberg_extend_block_editor_settings_with_default_editor_styles( $settings ) { +- $editor_styles_file = gutenberg_dir_path() . 'build/editor/editor-styles.css'; ++ $editor_styles_file = is_rtl() ? ++ gutenberg_dir_path() . 'build/editor/editor-styles-rtl.css' : ++ gutenberg_dir_path() . 'build/editor/editor-styles.css'; + $settings['defaultEditorStyles'] = array( + array( + 'css' => file_get_contents( $editor_styles_file ), +@@ -639,3 +669,79 @@ function gutenberg_extend_block_editor_settings_with_default_editor_styles( $set + return $settings; + } + add_filter( 'block_editor_settings', 'gutenberg_extend_block_editor_settings_with_default_editor_styles' ); ++ ++/** ++ * Adds a flag to the editor settings to know whether we're in FSE theme or not. ++ * ++ * @param array $settings Default editor settings. ++ * ++ * @return array Filtered editor settings. ++ */ ++function gutenberg_extend_block_editor_settings_with_fse_theme_flag( $settings ) { ++ $settings['isFSETheme'] = gutenberg_is_fse_theme(); ++ return $settings; ++} ++add_filter( 'block_editor_settings', 'gutenberg_extend_block_editor_settings_with_fse_theme_flag' ); ++ ++/** ++ * Sets the editor styles to be consumed by JS. ++ */ ++function gutenberg_extend_block_editor_styles_html() { ++ $handles = array( ++ 'wp-block-editor', ++ 'wp-block-library', ++ 'wp-edit-blocks', ++ ); ++ ++ $block_registry = WP_Block_Type_Registry::get_instance(); ++ ++ foreach ( $block_registry->get_all_registered() as $block_name => $block_type ) { ++ if ( ! empty( $block_type->style ) ) { ++ $handles[] = $block_type->style; ++ } ++ ++ if ( ! empty( $block_type->editor_style ) ) { ++ $handles[] = $block_type->editor_style; ++ } ++ } ++ ++ $handles = array_unique( $handles ); ++ $done = wp_styles()->done; ++ ++ ob_start(); ++ ++ wp_styles()->done = array(); ++ wp_styles()->do_items( $handles ); ++ wp_styles()->done = $done; ++ ++ $editor_styles = wp_json_encode( array( 'html' => ob_get_clean() ) ); ++ ++ echo ""; ++} ++add_action( 'admin_footer-toplevel_page_gutenberg-edit-site', 'gutenberg_extend_block_editor_styles_html' ); ++ ++/** ++ * Adds a polyfill for object-fit in environments which do not support it. ++ * ++ * The script registration occurs in `gutenberg_register_vendor_scripts`, which ++ * should be removed in coordination with this function. ++ * ++ * @see gutenberg_register_vendor_scripts ++ * @see https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit ++ * ++ * @since 9.1.0 ++ * ++ * @param WP_Scripts $scripts WP_Scripts object. ++ */ ++function gutenberg_add_object_fit_polyfill( $scripts ) { ++ did_action( 'init' ) && $scripts->add_inline_script( ++ 'wp-polyfill', ++ wp_get_script_polyfill( ++ $scripts, ++ array( ++ '"objectFit" in document.documentElement.style' => 'object-fit-polyfill', ++ ) ++ ) ++ ); ++} ++add_action( 'wp_default_scripts', 'gutenberg_add_object_fit_polyfill', 20 ); +I don't think we need any of this? God it's hard to say... +diff --git a/lib/compat.php b/lib/compat.php +index 2c4dfb9033..008298df27 100644 +--- a/lib/compat.php ++++ b/lib/compat.php +@@ -8,231 +8,6 @@ + * @package gutenberg + */ + +-/** +- * These functions can be removed when plugin support requires WordPress 5.5.0+. +- * +- * @see https://core.trac.wordpress.org/ticket/50263 +- * @see https://core.trac.wordpress.org/changeset/48141 +- */ +-if ( ! function_exists( 'register_block_type_from_metadata' ) ) { +- /** +- * 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 ( strpos( $asset_handle_or_path, $path_prefix ) !== 0 ) { +- 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|boolean 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 7.9.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. 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', +- // Deprecated: remove with Gutenberg 8.6 release. +- 'context' => '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 +- ) +- ); +- } +-} +- + /** + * Adds a wp.date.setSettings with timezone abbr parameter + * +@@ -305,163 +80,137 @@ function gutenberg_add_date_settings_timezone( $scripts ) { + add_action( 'wp_default_scripts', 'gutenberg_add_date_settings_timezone', 20 ); + + /** +- * Filters default block categories to substitute legacy category names with new +- * block categories. +- * +- * This can be removed when plugin support requires WordPress 5.5.0+. +- * +- * @see https://core.trac.wordpress.org/ticket/50278 +- * @see https://core.trac.wordpress.org/changeset/48177 +- * +- * @param array[] $default_categories Array of block categories. ++ * Determine if the current theme needs to load separate block styles or not. + * +- * @return array[] Filtered block categories. ++ * @return bool + */ +-function gutenberg_replace_default_block_categories( $default_categories ) { +- $substitution = array( +- 'common' => array( +- 'slug' => 'text', +- 'title' => __( 'Text', 'gutenberg' ), +- 'icon' => null, +- ), +- 'formatting' => array( +- 'slug' => 'media', +- 'title' => __( 'Media', 'gutenberg' ), +- 'icon' => null, +- ), +- 'layout' => array( +- 'slug' => 'design', +- 'title' => __( 'Design', 'gutenberg' ), +- 'icon' => null, +- ), +- ); +- +- // Loop default categories to perform in-place substitution by legacy slug. +- foreach ( $default_categories as $i => $default_category ) { +- $slug = $default_category['slug']; +- if ( isset( $substitution[ $slug ] ) ) { +- $default_categories[ $i ] = $substitution[ $slug ]; +- unset( $substitution[ $slug ] ); +- } +- } +- +- /* +- * At this point, `$substitution` should contain only the categories which +- * could not be in-place substituted with a default category, likely in the +- * case that core has since been updated to use the default categories. +- * Check to verify they exist. +- */ +- $default_category_slugs = wp_list_pluck( $default_categories, 'slug' ); +- foreach ( $substitution as $i => $substitute_category ) { +- if ( in_array( $substitute_category['slug'], $default_category_slugs, true ) ) { +- unset( $substitution[ $i ] ); +- } +- } +- +- /* +- * Any substitutes remaining should be appended, as they are not yet +- * assigned in the default categories array. ++function gutenberg_should_load_separate_block_styles() { ++ $load_separate_styles = gutenberg_is_fse_theme(); ++ /** ++ * Determine if separate styles will be loaded for blocks on-render or not. ++ * ++ * @param bool $load_separate_styles Whether separate styles will be loaded or not. ++ * ++ * @return bool + */ +- return array_merge( $default_categories, array_values( $substitution ) ); ++ return apply_filters( 'load_separate_block_styles', $load_separate_styles ); + } +-add_filter( 'block_categories', 'gutenberg_replace_default_block_categories' ); +- +-global $current_parsed_block; +-$current_parsed_block = array( +- 'blockName' => null, +- 'attributes' => null, +-); + + /** +- * Shim that hooks into `pre_render_block` so as to override `render_block` with +- * a function that assigns block context. +- * +- * The context handling can be removed when plugin support requires WordPress 5.5.0+. +- * The global current_parsed_block assignment can be removed when plugin support requires WordPress 5.6.0+. ++ * Remove the `wp_enqueue_registered_block_scripts_and_styles` hook if needed. + * +- * @see https://core.trac.wordpress.org/ticket/49927 +- * @see https://core.trac.wordpress.org/changeset/48243 +- * +- * @param string|null $pre_render The pre-rendered content. Defaults to null. +- * @param array $parsed_block The parsed block being rendered. +- * +- * @return string String of rendered HTML. ++ * @return void + */ +-function gutenberg_render_block_with_assigned_block_context( $pre_render, $parsed_block ) { +- global $post, $wp_query; +- global $current_parsed_block; +- +- /* +- * If a non-null value is provided, a filter has run at an earlier priority +- * and has already handled custom rendering and should take precedence. +- */ +- if ( null !== $pre_render ) { +- return $pre_render; ++function gutenberg_remove_hook_wp_enqueue_registered_block_scripts_and_styles() { ++ if ( gutenberg_should_load_separate_block_styles() ) { ++ /** ++ * Avoid enqueueing block assets of all registered blocks for all posts, instead ++ * deferring to block render mechanics to enqueue scripts, thereby ensuring only ++ * blocks of the content have their assets enqueued. ++ * ++ * This can be removed once minimum support for the plugin is outside the range ++ * of the version associated with closure of the following ticket. ++ * ++ * @see https://core.trac.wordpress.org/ticket/50328 ++ * ++ * @see WP_Block::render ++ */ ++ remove_action( 'enqueue_block_assets', 'wp_enqueue_registered_block_scripts_and_styles' ); + } ++} + +- $current_parsed_block = $parsed_block; +- +- $source_block = $parsed_block; +- +- /** This filter is documented in src/wp-includes/blocks.php */ +- $parsed_block = apply_filters( 'render_block_data', $parsed_block, $source_block ); +- +- $context = array(); +- +- if ( $post instanceof WP_Post ) { +- $context['postId'] = $post->ID; ++add_action( 'init', 'gutenberg_remove_hook_wp_enqueue_registered_block_scripts_and_styles' ); + +- /* +- * 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; +- } ++/** ++ * Callback hooked to the register_block_type_args filter. ++ * ++ * This hooks into block registration to inject the default context into the block object. ++ * It can be removed once the default context is added into Core. ++ * ++ * @param array $args Block attributes. ++ * @return array Block attributes. ++ */ ++function gutenberg_inject_default_block_context( $args ) { ++ if ( is_callable( $args['render_callback'] ) ) { ++ $block_render_callback = $args['render_callback']; ++ $args['render_callback'] = function( $attributes, $content, $block = null ) use ( $block_render_callback ) { ++ global $post, $wp_query; ++ ++ // Check for null for back compatibility with WP_Block_Type->render ++ // which is unused since the introduction of WP_Block class. ++ // ++ // See: ++ // - https://core.trac.wordpress.org/ticket/49927 ++ // - commit 910de8f6890c87f93359c6f2edc6c27b9a3f3292 at wordpress-develop. ++ ++ if ( null === $block ) { ++ return $block_render_callback( $attributes, $content ); ++ } + +- if ( isset( $wp_query->tax_query->queried_terms['category'] ) ) { +- $context['query'] = array( 'categoryIds' => array() ); ++ $registry = WP_Block_Type_Registry::get_instance(); ++ $block_type = $registry->get_registered( $block->name ); + +- 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; +- } +- } ++ // For WordPress versions that don't support the context API. ++ if ( ! $block->context ) { ++ $block->context = array(); ++ } + +- if ( isset( $wp_query->tax_query->queried_terms['post_tag'] ) ) { +- if ( isset( $context['query'] ) ) { +- $context['query']['tagIds'] = array(); +- } else { +- $context['query'] = array( 'tagIds' => array() ); +- } ++ // Inject the post context if not done by Core. ++ $needs_post_id = ! empty( $block_type->uses_context ) && in_array( 'postId', $block_type->uses_context, true ); ++ if ( $post instanceof WP_Post && $needs_post_id && ! isset( $block->context['postId'] ) && 'wp_template' !== $post->post_type && 'wp_template_part' !== $post->post_type ) { ++ $block->context['postId'] = $post->ID; ++ } ++ $needs_post_type = ! empty( $block_type->uses_context ) && in_array( 'postType', $block_type->uses_context, true ); ++ if ( $post instanceof WP_Post && $needs_post_type && ! isset( $block->context['postType'] ) && 'wp_template' !== $post->post_type && 'wp_template_part' !== $post->post_type ) { ++ /* ++ * 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. ++ */ ++ $block->context['postType'] = $post->post_type; ++ } + +- foreach ( $wp_query->tax_query->queried_terms['post_tag']['terms'] as $tag_slug_or_id ) { +- $tag_ID = $tag_slug_or_id; ++ // Inject the query context if not done by Core. ++ $needs_query = ! empty( $block_type->uses_context ) && in_array( 'query', $block_type->uses_context, true ); ++ if ( ! isset( $block->context['query'] ) && $needs_query ) { ++ if ( isset( $wp_query->tax_query->queried_terms['category'] ) ) { ++ $block->context['query'] = array( 'categoryIds' => array() ); + +- if ( 'slug' === $wp_query->tax_query->queried_terms['post_tag']['field'] ) { +- $tag = get_term_by( 'slug', $tag_slug_or_id, 'post_tag' ); ++ foreach ( $wp_query->tax_query->queried_terms['category']['terms'] as $category_slug_or_id ) { ++ $block->context['query']['categoryIds'][] = 'slug' === $wp_query->tax_query->queried_terms['category']['field'] ? get_cat_ID( $category_slug_or_id ) : $category_slug_or_id; ++ } ++ } + +- if ( $tag ) { +- $tag_ID = $tag->term_id; ++ if ( isset( $wp_query->tax_query->queried_terms['post_tag'] ) ) { ++ if ( isset( $block->context['query'] ) ) { ++ $block->context['query']['tagIds'] = array(); ++ } else { ++ $block->context['query'] = array( 'tagIds' => array() ); ++ } ++ ++ foreach ( $wp_query->tax_query->queried_terms['post_tag']['terms'] as $tag_slug_or_id ) { ++ $tag_ID = $tag_slug_or_id; ++ ++ if ( 'slug' === $wp_query->tax_query->queried_terms['post_tag']['field'] ) { ++ $tag = get_term_by( 'slug', $tag_slug_or_id, 'post_tag' ); ++ ++ if ( $tag ) { ++ $tag_ID = $tag->term_id; ++ } ++ } ++ $block->context['query']['tagIds'][] = $tag_ID; ++ } + } + } +- $context['query']['tagIds'][] = $tag_ID; +- } +- } +- +- /** +- * Filters the default context provided to a rendered block. +- * +- * @param array $context Default context. +- * @param array $parsed_block Block being rendered, filtered by `render_block_data`. +- */ +- $context = apply_filters( 'render_block_context', $context, $parsed_block ); + +- $block = new WP_Block( $parsed_block, $context ); +- +- return $block->render(); ++ return $block_render_callback( $attributes, $content, $block ); ++ }; ++ } ++ return $args; + } +-add_filter( 'pre_render_block', 'gutenberg_render_block_with_assigned_block_context', 9, 2 ); ++ ++add_filter( 'register_block_type_args', 'gutenberg_inject_default_block_context' ); + + /** + * Amends the paths to preload when initializing edit post. +Skip. FSE related. +diff --git a/lib/demo-block-template-parts/header.html b/lib/demo-block-template-parts/header.html +deleted file mode 100644 +index e4abb0f058..0000000000 +--- a/lib/demo-block-template-parts/header.html ++++ /dev/null +@@ -1,9 +0,0 @@ +- +- +- +- +- +- +- +- +- +\ No newline at end of file +diff --git a/lib/demo-block-templates/category.html b/lib/demo-block-templates/category.html +deleted file mode 100644 +index 3737864c6a..0000000000 +--- a/lib/demo-block-templates/category.html ++++ /dev/null +@@ -1,20 +0,0 @@ +- +-
+-
+- +-

+- Category Template +-

+- +-
+-
+- +- +- +- +- +- +- +diff --git a/lib/demo-block-templates/front-page.html b/lib/demo-block-templates/front-page.html +deleted file mode 100644 +index f150777c42..0000000000 +--- a/lib/demo-block-templates/front-page.html ++++ /dev/null +@@ -1,19 +0,0 @@ +- +- +- +-
+-

Say Hello to the New New Editor

+- +- +- +-

Now you can edit your entire site!

+-
+- +- +- +- +- +- +- +-

Rebuilding the entire editing experience pages and posts was just the start. We have expanded what is possible within the editor by making every part of your site customizable right in the editor. Customize your site navigation. Change your site title. Customize your footer. If you can imagine it, you can build it.

+- +diff --git a/lib/demo-block-templates/index.html b/lib/demo-block-templates/index.html +deleted file mode 100644 +index e6d07a97d3..0000000000 +--- a/lib/demo-block-templates/index.html ++++ /dev/null +@@ -1,12 +0,0 @@ +- +-
+-
+- +-
+-
+- +- +- +Skip. It says this is all already in Core. Only added setting is FSE related. +diff --git a/lib/editor-settings.php b/lib/editor-settings.php +new file mode 100644 +index 0000000000..e0970e761b +--- /dev/null ++++ b/lib/editor-settings.php +@@ -0,0 +1,82 @@ ++ __( 'Thumbnail', 'gutenberg' ), ++ 'medium' => __( 'Medium', 'gutenberg' ), ++ 'large' => __( 'Large', 'gutenberg' ), ++ 'full' => __( 'Full Size', 'gutenberg' ), ++ ) ++ ); ++ foreach ( $image_size_names as $image_size_slug => $image_size_name ) { ++ $available_image_sizes[] = array( ++ 'slug' => $image_size_slug, ++ 'name' => $image_size_name, ++ ); ++ }; ++ ++ $settings = array( ++ '__unstableEnableFullSiteEditingBlocks' => gutenberg_is_fse_theme(), ++ 'disableCustomColors' => get_theme_support( 'disable-custom-colors' ), ++ 'disableCustomFontSizes' => get_theme_support( 'disable-custom-font-sizes' ), ++ 'disableCustomGradients' => get_theme_support( 'disable-custom-gradients' ), ++ 'enableCustomLineHeight' => get_theme_support( 'custom-line-height' ), ++ 'enableCustomUnits' => get_theme_support( 'custom-units' ), ++ 'imageSizes' => $available_image_sizes, ++ 'isRTL' => is_rtl(), ++ 'maxUploadFileSize' => $max_upload_size, ++ ); ++ ++ $color_palette = current( (array) get_theme_support( 'editor-color-palette' ) ); ++ if ( false !== $color_palette ) { ++ $settings['colors'] = $color_palette; ++ } ++ ++ $font_sizes = current( (array) get_theme_support( 'editor-font-sizes' ) ); ++ if ( false !== $font_sizes ) { ++ $settings['fontSizes'] = $font_sizes; ++ } ++ ++ $gradient_presets = current( (array) get_theme_support( 'editor-gradient-presets' ) ); ++ if ( false !== $gradient_presets ) { ++ $settings['gradients'] = $gradient_presets; ++ } ++ ++ return $settings; ++} ++ ++/** ++ * Extends the block editor with settings that are only in the plugin. ++ * ++ * @param array $settings Existing editor settings. ++ * ++ * @return array Filtered settings. ++ */ ++function gutenberg_extend_post_editor_settings( $settings ) { ++ $settings['__unstableEnableFullSiteEditingBlocks'] = gutenberg_is_fse_theme(); ++ return $settings; ++} ++add_filter( 'block_editor_settings', 'gutenberg_extend_post_editor_settings' ); +Skip. FSE related. +diff --git a/lib/experimental-default-theme.json b/lib/experimental-default-theme.json +index 28a20292ff..1ad199567d 100644 +--- a/lib/experimental-default-theme.json ++++ b/lib/experimental-default-theme.json +@@ -134,37 +134,44 @@ + "dropCap": true, + "customFontSize": true, + "customLineHeight": false, ++ "customFontStyle": true, ++ "customFontWeight": true, ++ "customTextTransforms": true, ++ "customTextDecorations": true, + "fontSizes": [ + { + "name": "Small", + "slug": "small", +- "size": 13 ++ "size": "13px" + }, + { + "name": "Normal", + "slug": "normal", +- "size": 16 ++ "size": "16px" + }, + { + "name": "Medium", + "slug": "medium", +- "size": 20 ++ "size": "20px" + }, + { + "name": "Large", + "slug": "large", +- "size": 36 ++ "size": "36px" + }, + { + "name": "Huge", + "slug": "huge", +- "size": 42 ++ "size": "42px" + } + ] + }, + "spacing": { + "customPadding": false, + "units": [ "px", "em", "rem", "vh", "vw" ] ++ }, ++ "border": { ++ "customRadius": false + } + } + } +diff --git a/lib/experimental-i18n-theme.json b/lib/experimental-i18n-theme.json +new file mode 100644 +index 0000000000..4731a59df0 +--- /dev/null ++++ b/lib/experimental-i18n-theme.json +@@ -0,0 +1,32 @@ ++{ ++ "settings": { ++ "typography": { ++ "fontSizes": [ ++ "name" ++ ], ++ "fontStyles": [ ++ "name" ++ ], ++ "fontWeights": [ ++ "name" ++ ], ++ "fontFamilies": [ ++ "name" ++ ], ++ "textTransforms": [ ++ "name" ++ ], ++ "textDecorations": [ ++ "name" ++ ] ++ }, ++ "color": { ++ "palette": [ ++ "name" ++ ], ++ "gradients": [ ++ "name" ++ ] ++ } ++ } ++} +Skip. Plugin only. +diff --git a/lib/experiments-page.php b/lib/experiments-page.php +index 26cb98036b..fa35fa81dd 100644 +--- a/lib/experiments-page.php ++++ b/lib/experiments-page.php +@@ -51,28 +51,6 @@ function gutenberg_initialize_experiments_settings() { + 'id' => 'gutenberg-navigation', + ) + ); +- add_settings_field( +- 'gutenberg-full-site-editing', +- __( 'Full Site Editing', 'gutenberg' ), +- 'gutenberg_display_experiment_field', +- 'gutenberg-experiments', +- 'gutenberg_experiments_section', +- array( +- 'label' => __( 'Enable Full Site Editing (Warning: this will replace your theme and cause potentially irreversible changes to your site. We recommend using this only in a development environment.)', 'gutenberg' ), +- 'id' => 'gutenberg-full-site-editing', +- ) +- ); +- add_settings_field( +- 'gutenberg-full-site-editing-demo', +- __( 'Full Site Editing Demo Templates', 'gutenberg' ), +- 'gutenberg_display_experiment_field', +- 'gutenberg-experiments', +- 'gutenberg_experiments_section', +- array( +- 'label' => __( 'Enable Full Site Editing demo templates', 'gutenberg' ), +- 'id' => 'gutenberg-full-site-editing-demo', +- ) +- ); + register_setting( + 'gutenberg-experiments', + 'gutenberg-experiments' +@@ -110,25 +88,3 @@ function gutenberg_display_experiment_section() { + + gutenberg_is_experiment_enabled( 'gutenberg-full-site-editing' ), +- '__experimentalEnableFullSiteEditingDemo' => gutenberg_is_experiment_enabled( 'gutenberg-full-site-editing-demo' ), +- ); +- +- $gradient_presets = current( (array) get_theme_support( 'editor-gradient-presets' ) ); +- if ( false !== $gradient_presets ) { +- $experiments_settings['gradients'] = $gradient_presets; +- } +- +- return array_merge( $settings, $experiments_settings ); +-} +-add_filter( 'block_editor_settings', 'gutenberg_experiments_editor_settings' ); +Skip. FSE related. +diff --git a/lib/full-site-editing/block-templates.php b/lib/full-site-editing/block-templates.php +new file mode 100644 +index 0000000000..bb5d26157f +--- /dev/null ++++ b/lib/full-site-editing/block-templates.php +@@ -0,0 +1,337 @@ ++ $file ) { ++ $path_list[] = $path; ++ } ++ } ++ return $path_list; ++} ++ ++/** ++ * Retrieves the template file from the theme for a given slug. ++ * ++ * @access private ++ * @internal ++ * ++ * @param array $template_type wp_template or wp_template_part. ++ * @param string $slug template slug. ++ * ++ * @return array Template. ++ */ ++function _gutenberg_get_template_file( $template_type, $slug ) { ++ $template_base_paths = array( ++ 'wp_template' => 'block-templates', ++ 'wp_template_part' => 'block-template-parts', ++ ); ++ $themes = array( ++ get_stylesheet() => get_stylesheet_directory(), ++ get_template() => get_template_directory(), ++ ); ++ foreach ( $themes as $theme_slug => $theme_dir ) { ++ $file_path = $theme_dir . '/' . $template_base_paths[ $template_type ] . '/' . $slug . '.html'; ++ if ( file_exists( $file_path ) ) { ++ return array( ++ 'slug' => $slug, ++ 'path' => $file_path, ++ 'theme' => $theme_slug, ++ 'type' => $template_type, ++ ); ++ } ++ } ++ ++ return null; ++} ++ ++/** ++ * Retrieves the template files from the theme. ++ * ++ * @access private ++ * @internal ++ * ++ * @param array $template_type wp_template or wp_template_part. ++ * ++ * @return array Template. ++ */ ++function _gutenberg_get_template_files( $template_type ) { ++ $template_base_paths = array( ++ 'wp_template' => 'block-templates', ++ 'wp_template_part' => 'block-template-parts', ++ ); ++ $themes = array( ++ get_stylesheet() => get_stylesheet_directory(), ++ get_template() => get_template_directory(), ++ ); ++ ++ $template_files = array(); ++ foreach ( $themes as $theme_slug => $theme_dir ) { ++ $theme_template_files = _gutenberg_get_template_paths( $theme_dir . '/' . $template_base_paths[ $template_type ] ); ++ foreach ( $theme_template_files as $template_file ) { ++ $template_base_path = $template_base_paths[ $template_type ]; ++ $template_slug = substr( ++ $template_file, ++ // Starting position of slug. ++ strpos( $template_file, $template_base_path . DIRECTORY_SEPARATOR ) + 1 + strlen( $template_base_path ), ++ // Subtract ending '.html'. ++ -5 ++ ); ++ $template_files[] = array( ++ 'slug' => $template_slug, ++ 'path' => $template_file, ++ 'theme' => $theme_slug, ++ 'type' => $template_type, ++ ); ++ } ++ } ++ ++ return $template_files; ++} ++ ++/** ++ * Build a unified template object based on a theme file. ++ * ++ * @param array $template_file Theme file. ++ * @param array $template_type wp_template or wp_template_part. ++ * ++ * @return WP_Block_Template Template. ++ */ ++function _gutenberg_build_template_result_from_file( $template_file, $template_type ) { ++ $default_template_types = gutenberg_get_default_template_types(); ++ ++ $theme = wp_get_theme()->get_stylesheet(); ++ $template = new WP_Block_Template(); ++ $template->id = $theme . '//' . $template_file['slug']; ++ $template->theme = $theme; ++ $template->content = file_get_contents( $template_file['path'] ); ++ $template->slug = $template_file['slug']; ++ $template->is_custom = false; ++ $template->type = $template_type; ++ $template->title = $template_file['slug']; ++ $template->status = 'publish'; ++ ++ if ( 'wp_template' === $template_type && isset( $default_template_types[ $template_file['slug'] ] ) ) { ++ $template->description = $default_template_types[ $template_file['slug'] ]['description']; ++ $template->title = $default_template_types[ $template_file['slug'] ]['title']; ++ } ++ ++ return $template; ++} ++ ++/** ++ * Build a unified template object based a post Object. ++ * ++ * @param WP_Post $post Template post. ++ * ++ * @return WP_Block_Template|WP_Error Template. ++ */ ++function _gutenberg_build_template_result_from_post( $post ) { ++ $terms = get_the_terms( $post, 'wp_theme' ); ++ ++ if ( is_wp_error( $terms ) ) { ++ return $terms; ++ } ++ ++ if ( ! $terms ) { ++ return new WP_Error( 'template_missing_theme', __( 'No theme is defined for this template.', 'gutenberg' ) ); ++ } ++ ++ $theme = $terms[0]->name; ++ ++ $template = new WP_Block_Template(); ++ $template->wp_id = $post->ID; ++ $template->id = $theme . '//' . $post->post_name; ++ $template->theme = $theme; ++ $template->content = $post->post_content; ++ $template->slug = $post->post_name; ++ $template->is_custom = true; ++ $template->type = $post->post_type; ++ $template->description = $post->post_excerpt; ++ $template->title = $post->post_title; ++ $template->status = $post->post_status; ++ ++ return $template; ++} ++ ++/** ++ * Retrieves a list of unified template objects based on a query. ++ * ++ * @param array $query { ++ * Optional. Arguments to retrieve templates. ++ * ++ * @type array $slug__in List of slugs to include. ++ * @type int $wp_id Post ID of customized template. ++ * } ++ * @param array $template_type wp_template or wp_template_part. ++ * ++ * @return array Templates. ++ */ ++function gutenberg_get_block_templates( $query = array(), $template_type = 'wp_template' ) { ++ $wp_query_args = array( ++ 'post_status' => array( 'auto-draft', 'draft', 'publish' ), ++ 'post_type' => $template_type, ++ 'posts_per_page' => -1, ++ 'no_found_rows' => true, ++ 'tax_query' => array( ++ array( ++ 'taxonomy' => 'wp_theme', ++ 'field' => 'name', ++ 'terms' => wp_get_theme()->get_stylesheet(), ++ ), ++ ), ++ ); ++ ++ if ( isset( $query['slug__in'] ) ) { ++ $wp_query_args['post_name__in'] = $query['slug__in']; ++ } ++ ++ // This is only needed for the regular templates/template parts CPT listing and editor. ++ if ( isset( $query['wp_id'] ) ) { ++ $wp_query_args['p'] = $query['wp_id']; ++ } else { ++ $wp_query_args['post_status'] = 'publish'; ++ } ++ ++ $template_query = new WP_Query( $wp_query_args ); ++ $query_result = array(); ++ foreach ( $template_query->get_posts() as $post ) { ++ $template = _gutenberg_build_template_result_from_post( $post ); ++ ++ if ( ! is_wp_error( $template ) ) { ++ $query_result[] = $template; ++ } ++ } ++ ++ if ( ! isset( $query['wp_id'] ) ) { ++ $template_files = _gutenberg_get_template_files( $template_type ); ++ foreach ( $template_files as $template_file ) { ++ $is_custom = array_search( ++ wp_get_theme()->get_stylesheet() . '//' . $template_file['slug'], ++ array_column( $query_result, 'id' ), ++ true ++ ); ++ $should_include = false === $is_custom && ( ++ ! isset( $query['slug__in'] ) || in_array( $template_file['slug'], $query['slug__in'], true ) ++ ); ++ if ( $should_include ) { ++ $query_result[] = _gutenberg_build_template_result_from_file( $template_file, $template_type ); ++ } ++ } ++ } ++ ++ return $query_result; ++} ++ ++/** ++ * Retrieves a single unified template object using its id. ++ * ++ * @param string $id Template unique identifier (example: theme|slug). ++ * @param array $template_type wp_template or wp_template_part. ++ * ++ * @return WP_Block_Template|null Template. ++ */ ++function gutenberg_get_block_template( $id, $template_type = 'wp_template' ) { ++ $parts = explode( '//', $id, 2 ); ++ if ( count( $parts ) < 2 ) { ++ return null; ++ } ++ list( $theme, $slug ) = $parts; ++ $wp_query_args = array( ++ 'name' => $slug, ++ 'post_type' => $template_type, ++ 'post_status' => array( 'auto-draft', 'draft', 'publish', 'trash' ), ++ 'posts_per_page' => 1, ++ 'no_found_rows' => true, ++ 'tax_query' => array( ++ array( ++ 'taxonomy' => 'wp_theme', ++ 'field' => 'name', ++ 'terms' => $theme, ++ ), ++ ), ++ ); ++ $template_query = new WP_Query( $wp_query_args ); ++ $posts = $template_query->get_posts(); ++ ++ if ( count( $posts ) > 0 ) { ++ $template = _gutenberg_build_template_result_from_post( $posts[0] ); ++ ++ if ( ! is_wp_error( $template ) ) { ++ return $template; ++ } ++ } ++ ++ if ( wp_get_theme()->get_stylesheet() === $theme ) { ++ $template_file = _gutenberg_get_template_file( $template_type, $slug ); ++ if ( null !== $template_file ) { ++ return _gutenberg_build_template_result_from_file( $template_file, $template_type ); ++ } ++ } ++ ++ return null; ++} ++ ++/** ++ * Generates a unique slug for templates or template parts. ++ * ++ * @param string $slug The resolved slug (post_name). ++ * @param int $post_ID Post ID. ++ * @param string $post_status No uniqueness checks are made if the post is still draft or pending. ++ * @param string $post_type Post type. ++ * @return string The original, desired slug. ++ */ ++function gutenberg_filter_wp_template_unique_post_slug( $slug, $post_ID, $post_status, $post_type ) { ++ if ( 'wp_template' !== $post_type || 'wp_template_part' !== $post_type ) { ++ return $slug; ++ } ++ ++ // Template slugs must be unique within the same theme. ++ $theme = get_the_terms( $post_ID, 'wp_theme' )[0]->slug; ++ ++ $check_query_args = array( ++ 'post_name' => $slug, ++ 'post_type' => $post_type, ++ 'posts_per_page' => 1, ++ 'post__not_in' => $post_ID, ++ 'tax_query' => array( ++ 'taxonomy' => 'wp_theme', ++ 'field' => 'name', ++ 'terms' => $theme, ++ ), ++ 'no_found_rows' => true, ++ ); ++ $check_query = new WP_Query( $check_query_args ); ++ $posts = $check_query->get_posts(); ++ ++ if ( count( $posts ) > 0 ) { ++ $suffix = 2; ++ do { ++ $query_args = $check_query_args; ++ $alt_post_name = _truncate_post_slug( $slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix"; ++ $query_args['post_name'] = $alt_post_name; ++ $query = new WP_Query( $check_query_args ); ++ $suffix++; ++ } while ( count( $query->get_posts() ) > 0 ); ++ $slug = $alt_post_name; ++ } ++ ++ return $slug; ++} ++add_filter( 'wp_unique_post_slug', 'gutenberg_filter_wp_template_unique_post_slug', 10, 4 ); +diff --git a/lib/full-site-editing/class-wp-block-template.php b/lib/full-site-editing/class-wp-block-template.php +new file mode 100644 +index 0000000000..b5e051241b +--- /dev/null ++++ b/lib/full-site-editing/class-wp-block-template.php +@@ -0,0 +1,83 @@ ++post_type = $post_type; ++ $this->namespace = 'wp/v2'; ++ $obj = get_post_type_object( $post_type ); ++ $this->rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name; ++ } ++ ++ /** ++ * Registers the controllers routes. ++ * ++ * @return void ++ */ ++ public function register_routes() { ++ // Lists all templates. ++ register_rest_route( ++ $this->namespace, ++ '/' . $this->rest_base, ++ array( ++ array( ++ 'methods' => WP_REST_Server::READABLE, ++ 'callback' => array( $this, 'get_items' ), ++ 'permission_callback' => array( $this, 'get_items_permissions_check' ), ++ 'args' => $this->get_collection_params(), ++ ), ++ array( ++ 'methods' => WP_REST_Server::CREATABLE, ++ 'callback' => array( $this, 'create_item' ), ++ 'permission_callback' => array( $this, 'create_item_permissions_check' ), ++ 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), ++ ), ++ 'schema' => array( $this, 'get_public_item_schema' ), ++ ) ++ ); ++ ++ // Lists/updates a single template based on the given id. ++ register_rest_route( ++ $this->namespace, ++ '/' . $this->rest_base . '/(?P[\/\w-]+)', ++ array( ++ array( ++ 'methods' => WP_REST_Server::READABLE, ++ 'callback' => array( $this, 'get_item' ), ++ 'permission_callback' => array( $this, 'get_item_permissions_check' ), ++ 'args' => array( ++ 'id' => array( ++ 'description' => __( 'The id of a template', 'gutenberg' ), ++ 'type' => 'string', ++ ), ++ ), ++ ), ++ array( ++ 'methods' => WP_REST_Server::EDITABLE, ++ 'callback' => array( $this, 'update_item' ), ++ 'permission_callback' => array( $this, 'update_item_permissions_check' ), ++ 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ++ ), ++ array( ++ 'methods' => WP_REST_Server::DELETABLE, ++ 'callback' => array( $this, 'delete_item' ), ++ 'permission_callback' => array( $this, 'delete_item_permissions_check' ), ++ 'args' => array( ++ 'force' => array( ++ 'type' => 'boolean', ++ 'default' => false, ++ 'description' => __( 'Whether to bypass Trash and force deletion.', 'gutenberg' ), ++ ), ++ ), ++ ), ++ 'schema' => array( $this, 'get_public_item_schema' ), ++ ) ++ ); ++ } ++ ++ /** ++ * Checks if the user has permissions to make the request. ++ * ++ * @return true|WP_Error True if the request has read access, WP_Error object otherwise. ++ */ ++ protected function permissions_check() { ++ // Verify if the current user has edit_theme_options capability. ++ // This capability is required to edit/view/delete templates. ++ if ( ! current_user_can( 'edit_theme_options' ) ) { ++ return new WP_Error( ++ 'rest_cannot_manage_templates', ++ __( 'Sorry, you are not allowed to access the templates on this site.', 'gutenberg' ), ++ array( ++ 'status' => rest_authorization_required_code(), ++ ) ++ ); ++ } ++ ++ return true; ++ } ++ ++ /** ++ * Checks if a given request has access to read templates. ++ * ++ * @param WP_REST_Request $request Full details about the request. ++ * @return true|WP_Error True if the request has read access, WP_Error object otherwise. ++ */ ++ public function get_items_permissions_check( $request ) { ++ return $this->permissions_check( $request ); ++ } ++ ++ /** ++ * Returns a list of templates. ++ * ++ * @param WP_REST_Request $request The request instance. ++ * ++ * @return WP_REST_Response ++ */ ++ public function get_items( $request ) { ++ $query = array(); ++ if ( isset( $request['wp_id'] ) ) { ++ $query['wp_id'] = $request['wp_id']; ++ } ++ $templates = array(); ++ foreach ( gutenberg_get_block_templates( $query, $this->post_type ) as $template ) { ++ $data = $this->prepare_item_for_response( $template, $request ); ++ $templates[] = $this->prepare_response_for_collection( $data ); ++ } ++ ++ return rest_ensure_response( $templates ); ++ } ++ ++ /** ++ * Checks if a given request has access to read a single template. ++ * ++ * @param WP_REST_Request $request Full details about the request. ++ * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise. ++ */ ++ public function get_item_permissions_check( $request ) { ++ return $this->permissions_check( $request ); ++ } ++ ++ /** ++ * Returns the given template ++ * ++ * @param WP_REST_Request $request The request instance. ++ * ++ * @return WP_REST_Response|WP_Error ++ */ ++ public function get_item( $request ) { ++ $template = gutenberg_get_block_template( $request['id'], $this->post_type ); ++ ++ if ( ! $template ) { ++ return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.', 'gutenberg' ), array( 'status' => 404 ) ); ++ } ++ ++ return $this->prepare_item_for_response( $template, $request ); ++ } ++ ++ /** ++ * Checks if a given request has access to write a single template. ++ * ++ * @param WP_REST_Request $request Full details about the request. ++ * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise. ++ */ ++ public function update_item_permissions_check( $request ) { ++ return $this->permissions_check( $request ); ++ } ++ ++ /** ++ * Updates a single template. ++ * ++ * @param WP_REST_Request $request Full details about the request. ++ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. ++ */ ++ public function update_item( $request ) { ++ $template = gutenberg_get_block_template( $request['id'], $this->post_type ); ++ if ( ! $template ) { ++ return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.', 'gutenberg' ), array( 'status' => 404 ) ); ++ } ++ ++ $changes = $this->prepare_item_for_database( $request ); ++ ++ if ( $template->is_custom ) { ++ $result = wp_update_post( wp_slash( (array) $changes ), true ); ++ } else { ++ $result = wp_insert_post( wp_slash( (array) $changes ), true ); ++ } ++ if ( is_wp_error( $result ) ) { ++ return $result; ++ } ++ ++ $template = gutenberg_get_block_template( $request['id'], $this->post_type ); ++ $fields_update = $this->update_additional_fields_for_object( $template, $request ); ++ if ( is_wp_error( $fields_update ) ) { ++ return $fields_update; ++ } ++ ++ return $this->prepare_item_for_response( ++ gutenberg_get_block_template( $request['id'], $this->post_type ), ++ $request ++ ); ++ } ++ ++ /** ++ * Checks if a given request has access to create a template. ++ * ++ * @param WP_REST_Request $request Full details about the request. ++ * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise. ++ */ ++ public function create_item_permissions_check( $request ) { ++ return $this->permissions_check( $request ); ++ } ++ ++ /** ++ * Creates a single template. ++ * ++ * @param WP_REST_Request $request Full details about the request. ++ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. ++ */ ++ public function create_item( $request ) { ++ $changes = $this->prepare_item_for_database( $request ); ++ $changes->post_name = $request['slug']; ++ $result = wp_insert_post( wp_slash( (array) $changes ), true ); ++ if ( is_wp_error( $result ) ) { ++ return $result; ++ } ++ $posts = gutenberg_get_block_templates( array( 'wp_id' => $result ), $this->post_type ); ++ if ( ! count( $posts ) ) { ++ return new WP_Error( 'rest_template_insert_error', __( 'No templates exist with that id.', 'gutenberg' ) ); ++ } ++ $id = $posts[0]->id; ++ $template = gutenberg_get_block_template( $id, $this->post_type ); ++ $fields_update = $this->update_additional_fields_for_object( $template, $request ); ++ if ( is_wp_error( $fields_update ) ) { ++ return $fields_update; ++ } ++ ++ return $this->prepare_item_for_response( ++ gutenberg_get_block_template( $id, $this->post_type ), ++ $request ++ ); ++ } ++ ++ /** ++ * Checks if a given request has access to delete a single template. ++ * ++ * @param WP_REST_Request $request Full details about the request. ++ * @return true|WP_Error True if the request has dedlete access for the item, WP_Error object otherwise. ++ */ ++ public function delete_item_permissions_check( $request ) { ++ return $this->permissions_check( $request ); ++ } ++ ++ /** ++ * Deletes a single template. ++ * ++ * @param WP_REST_Request $request Full details about the request. ++ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. ++ */ ++ public function delete_item( $request ) { ++ $template = gutenberg_get_block_template( $request['id'], $this->post_type ); ++ if ( ! $template ) { ++ return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.', 'gutenberg' ), array( 'status' => 404 ) ); ++ } ++ if ( ! $template->is_custom ) { ++ return new WP_Error( 'rest_invalid_template', __( 'Templates based on theme files can\'t be removed.', 'gutenberg' ), array( 'status' => 400 ) ); ++ } ++ ++ $id = $template->wp_id; ++ $force = (bool) $request['force']; ++ ++ // If we're forcing, then delete permanently. ++ if ( $force ) { ++ $previous = $this->prepare_item_for_response( $template, $request ); ++ wp_delete_post( $id, true ); ++ $response = new WP_REST_Response(); ++ $response->set_data( ++ array( ++ 'deleted' => true, ++ 'previous' => $previous->get_data(), ++ ) ++ ); ++ ++ return $response; ++ } ++ ++ // Otherwise, only trash if we haven't already. ++ if ( 'trash' === $template->status ) { ++ return new WP_Error( ++ 'rest_template_already_trashed', ++ __( 'The template has already been deleted.', 'gutenberg' ), ++ array( 'status' => 410 ) ++ ); ++ } ++ ++ wp_trash_post( $id ); ++ $template->status = 'trash'; ++ return $this->prepare_item_for_response( $template, $request ); ++ } ++ ++ /** ++ * Prepares a single template for create or update. ++ * ++ * @param WP_REST_Request $request Request object. ++ * @return stdClass Changes to pass to wp_update_post. ++ */ ++ protected function prepare_item_for_database( $request ) { ++ $template = $request['id'] ? gutenberg_get_block_template( $request['id'], $this->post_type ) : null; ++ $changes = new stdClass(); ++ $changes->post_name = $template->slug; ++ if ( null === $template ) { ++ $changes->post_type = $this->post_type; ++ $changes->post_status = 'publish'; ++ $changes->tax_input = array( ++ 'wp_theme' => isset( $request['theme'] ) ? $request['content'] : wp_get_theme()->get_stylesheet(), ++ ); ++ } elseif ( ! $template->is_custom ) { ++ $changes->post_type = $this->post_type; ++ $changes->post_status = 'publish'; ++ $changes->tax_input = array( ++ 'wp_theme' => $template->theme, ++ ); ++ } else { ++ $changes->ID = $template->wp_id; ++ $changes->post_status = 'publish'; ++ } ++ if ( isset( $request['content'] ) ) { ++ $changes->post_content = $request['content']; ++ } elseif ( null !== $template && ! $template->is_custom ) { ++ $changes->post_content = $template->content; ++ } ++ if ( isset( $request['title'] ) ) { ++ $changes->post_title = $request['title']; ++ } elseif ( null !== $template && ! $template->is_custom ) { ++ $changes->post_title = $template->title; ++ } ++ if ( isset( $request['description'] ) ) { ++ $changes->post_excerpt = $request['description']; ++ } elseif ( null !== $template && ! $template->is_custom ) { ++ $changes->post_excerpt = $template->description; ++ } ++ ++ return $changes; ++ } ++ ++ /** ++ * Prepare a single template output for response ++ * ++ * @param WP_Block_Template $template Template instance. ++ * @param WP_REST_Request $request Request object. ++ * ++ * @return WP_REST_Response $data ++ */ ++ public function prepare_item_for_response( $template, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable ++ $result = array( ++ 'id' => $template->id, ++ 'theme' => $template->theme, ++ 'content' => array( 'raw' => $template->content ), ++ 'slug' => $template->slug, ++ 'is_custom' => $template->is_custom, ++ 'type' => $template->type, ++ 'description' => $template->description, ++ 'title' => array( ++ 'raw' => $template->title, ++ 'rendered' => $template->title, ++ ), ++ 'status' => $template->status, ++ 'wp_id' => $template->wp_id, ++ ); ++ ++ $result = $this->add_additional_fields_to_object( $result, $request ); ++ ++ $response = rest_ensure_response( $result ); ++ $links = $this->prepare_links( $template->id ); ++ $response->add_links( $links ); ++ if ( ! empty( $links['self']['href'] ) ) { ++ $actions = $this->get_available_actions(); ++ $self = $links['self']['href']; ++ foreach ( $actions as $rel ) { ++ $response->add_link( $rel, $self ); ++ } ++ } ++ ++ return $response; ++ } ++ ++ ++ /** ++ * Prepares links for the request. ++ * ++ * @param integer $id ID. ++ * @return array Links for the given post. ++ */ ++ protected function prepare_links( $id ) { ++ $base = sprintf( '%s/%s', $this->namespace, $this->rest_base ); ++ ++ $links = array( ++ 'self' => array( ++ 'href' => rest_url( trailingslashit( $base ) . $id ), ++ ), ++ 'collection' => array( ++ 'href' => rest_url( $base ), ++ ), ++ 'about' => array( ++ 'href' => rest_url( 'wp/v2/types/' . $this->post_type ), ++ ), ++ ); ++ ++ return $links; ++ } ++ ++ /** ++ * Get the link relations available for the post and current user. ++ * ++ * @return array List of link relations. ++ */ ++ protected function get_available_actions() { ++ $rels = array(); ++ ++ $post_type = get_post_type_object( $this->post_type ); ++ ++ if ( current_user_can( $post_type->cap->publish_posts ) ) { ++ $rels[] = 'https://api.w.org/action-publish'; ++ } ++ ++ if ( current_user_can( 'unfiltered_html' ) ) { ++ $rels[] = 'https://api.w.org/action-unfiltered-html'; ++ } ++ ++ return $rels; ++ } ++ ++ /** ++ * Retrieves the query params for the posts collection. ++ * ++ * @return array Collection parameters. ++ */ ++ public function get_collection_params() { ++ return array( ++ 'context' => $this->get_context_param(), ++ 'wp_id' => array( ++ 'description' => __( 'Limit to the specified post id.', 'gutenberg' ), ++ 'type' => 'integer', ++ ), ++ ); ++ } ++ ++ /** ++ * Retrieves the block type' schema, conforming to JSON Schema. ++ * ++ * @return array Item schema data. ++ */ ++ public function get_item_schema() { ++ if ( $this->schema ) { ++ return $this->add_additional_fields_schema( $this->schema ); ++ } ++ ++ $schema = array( ++ '$schema' => 'http://json-schema.org/draft-04/schema#', ++ 'title' => $this->post_type, ++ 'type' => 'object', ++ 'properties' => array( ++ 'id' => array( ++ 'description' => __( 'ID of template.', 'gutenberg' ), ++ 'type' => 'string', ++ 'context' => array( 'embed', 'view', 'edit' ), ++ 'readonly' => true, ++ ), ++ 'slug' => array( ++ 'description' => __( 'Unique slug identifying the template.', 'gutenberg' ), ++ 'type' => 'string', ++ 'context' => array( 'embed', 'view', 'edit' ), ++ 'required' => true, ++ 'minLength' => 1, ++ 'pattern' => '[a-zA-Z_\-]+', ++ ), ++ 'theme' => array( ++ 'description' => __( 'Theme identifier for the template.', 'gutenberg' ), ++ 'type' => 'string', ++ 'context' => array( 'embed', 'view', 'edit' ), ++ ), ++ 'is_custom' => array( ++ 'description' => __( 'Whether the template is customized.', 'gutenberg' ), ++ 'type' => 'bool', ++ 'context' => array( 'embed', 'view', 'edit' ), ++ 'readonly' => true, ++ ), ++ 'content' => array( ++ 'description' => __( 'Content of template.', 'gutenberg' ), ++ 'type' => array( 'object', 'string' ), ++ 'default' => '', ++ 'context' => array( 'embed', 'view', 'edit' ), ++ ), ++ 'title' => array( ++ 'description' => __( 'Title of template.', 'gutenberg' ), ++ 'type' => array( 'object', 'string' ), ++ 'default' => '', ++ 'context' => array( 'embed', 'view', 'edit' ), ++ ), ++ 'description' => array( ++ 'description' => __( 'Description of template.', 'gutenberg' ), ++ 'type' => 'string', ++ 'default' => '', ++ 'context' => array( 'embed', 'view', 'edit' ), ++ ), ++ 'status' => array( ++ 'description' => __( 'Status of template.', 'gutenberg' ), ++ 'type' => 'string', ++ 'default' => 'publish', ++ 'context' => array( 'embed', 'view', 'edit' ), ++ ), ++ 'wp_id' => array( ++ 'description' => __( 'Post ID.', 'gutenberg' ), ++ 'type' => 'integer', ++ 'context' => array( 'embed', 'view', 'edit' ), ++ 'readonly' => true, ++ ), ++ ), ++ ); ++ ++ $this->schema = $schema; ++ ++ return $this->add_additional_fields_schema( $this->schema ); ++ } ++} +diff --git a/lib/full-site-editing/default-template-types.php b/lib/full-site-editing/default-template-types.php +new file mode 100644 +index 0000000000..54cc722be9 +--- /dev/null ++++ b/lib/full-site-editing/default-template-types.php +@@ -0,0 +1,128 @@ ++ array( ++ 'title' => _x( 'Index', 'Template name', 'gutenberg' ), ++ 'description' => __( 'The default template which is used when no other template can be found', 'gutenberg' ), ++ ), ++ 'home' => array( ++ 'title' => _x( 'Home', 'Template name', 'gutenberg' ), ++ 'description' => __( 'The home page template, which is the front page by default. If you use a static front page this is the template for the page with the latest posts', 'gutenberg' ), ++ ), ++ 'front-page' => array( ++ 'title' => _x( 'Front Page', 'Template name', 'gutenberg' ), ++ 'description' => __( 'Used when the site home page is queried', 'gutenberg' ), ++ ), ++ 'singular' => array( ++ 'title' => _x( 'Singular', 'Template name', 'gutenberg' ), ++ 'description' => __( 'Used when a single entry is queried. This template will be overridden the Single, Post, and Page templates where appropriate', 'gutenberg' ), ++ ), ++ 'single' => array( ++ 'title' => _x( 'Single', 'Template name', 'gutenberg' ), ++ 'description' => __( 'Used when a single entry that is not a Page is queried', 'gutenberg' ), ++ ), ++ 'single-post' => array( ++ 'title' => _x( 'Post', 'Template name', 'gutenberg' ), ++ 'description' => __( 'Used when a single Post is queried', 'gutenberg' ), ++ ), ++ 'page' => array( ++ 'title' => _x( 'Page', 'Template name', 'gutenberg' ), ++ 'description' => __( 'Used when an individual Page is queried', 'gutenberg' ), ++ ), ++ 'archive' => array( ++ 'title' => _x( 'Archive', 'Template name', 'gutenberg' ), ++ 'description' => __( 'Used when multiple entries are queried. This template will be overridden the Category, Author, and Date templates where appropriate', 'gutenberg' ), ++ ), ++ 'author' => array( ++ 'title' => _x( 'Author Archive', 'Template name', 'gutenberg' ), ++ 'description' => __( 'Used when a list of Posts from a single author is queried', 'gutenberg' ), ++ ), ++ 'category' => array( ++ 'title' => _x( 'Post Category Archive', 'Template name', 'gutenberg' ), ++ 'description' => __( 'Used when a list of Posts from a category is queried', 'gutenberg' ), ++ ), ++ 'taxonomy' => array( ++ 'title' => _x( 'Taxonomy Archive', 'Template name', 'gutenberg' ), ++ 'description' => __( 'Used when a list of posts from a taxonomy is queried', 'gutenberg' ), ++ ), ++ 'date' => array( ++ 'title' => _x( 'Date Archive', 'Template name', 'gutenberg' ), ++ 'description' => __( 'Used when a list of Posts from a certain date are queried', 'gutenberg' ), ++ ), ++ 'tag' => array( ++ 'title' => _x( 'Tag Archive', 'Template name', 'gutenberg' ), ++ 'description' => __( 'Used when a list of Posts with a certain tag is queried', 'gutenberg' ), ++ ), ++ 'attachment' => array( ++ 'title' => __( 'Media', 'gutenberg' ), ++ 'description' => __( 'Used when a Media entry is queried', 'gutenberg' ), ++ ), ++ 'search' => array( ++ 'title' => _x( 'Search Results', 'Template name', 'gutenberg' ), ++ 'description' => __( 'Used when a visitor searches the site', 'gutenberg' ), ++ ), ++ 'privacy-policy' => array( ++ 'title' => __( 'Privacy Policy', 'gutenberg' ), ++ 'description' => '', ++ ), ++ '404' => array( ++ 'title' => _x( '404', 'Template name', 'gutenberg' ), ++ 'description' => __( 'Used when the queried content cannot be found', 'gutenberg' ), ++ ), ++ ); ++ ++ /** ++ * Filters the list of template types. ++ * ++ * @param array $default_template_types An array of template types, formatted as [ slug => [ title, description ] ]. ++ * ++ * @since 5.x.x ++ */ ++ return apply_filters( 'default_template_types', $default_template_types ); ++} ++ ++/** ++ * Converts the default template types array from associative to indexed, ++ * to be used in JS, where numeric keys (e.g. '404') always appear before alphabetical ++ * ones, regardless of the actual order of the array. ++ * ++ * From slug-keyed associative array: ++ * `[ 'index' => [ 'title' => 'Index', 'description' => 'Index template' ] ]` ++ * ++ * ...to indexed array with slug as property: ++ * `[ [ 'slug' => 'index', 'title' => 'Index', 'description' => 'Index template' ] ]` ++ * ++ * @return array The default template types as an indexed array. ++ */ ++function gutenberg_get_indexed_default_template_types() { ++ $indexed_template_types = array(); ++ $default_template_types = gutenberg_get_default_template_types(); ++ foreach ( $default_template_types as $slug => $template_type ) { ++ $template_type['slug'] = (string) $slug; ++ $indexed_template_types[] = $template_type; ++ } ++ return $indexed_template_types; ++} ++ ++/** ++ * Return a list of all overrideable default template type slugs. ++ * ++ * @see get_query_template ++ * ++ * @return string[] List of all overrideable default template type slugs. ++ */ ++function gutenberg_get_template_type_slugs() { ++ return array_keys( gutenberg_get_default_template_types() ); ++} +diff --git a/lib/edit-site-export.php b/lib/full-site-editing/edit-site-export.php +similarity index 57% +rename from lib/edit-site-export.php +rename to lib/full-site-editing/edit-site-export.php +index c62fb42c2c..b299b51e16 100644 +--- a/lib/edit-site-export.php ++++ b/lib/full-site-editing/edit-site-export.php +@@ -12,29 +12,31 @@ + function gutenberg_edit_site_export() { + // Create ZIP file and directories. + $filename = tempnam( get_temp_dir(), 'edit-site-export' ); +- $zip = new ZipArchive(); ++ if ( ! class_exists( 'ZipArchive' ) ) { ++ return new WP_Error( 'Zip Export not supported.' ); ++ } ++ $zip = new ZipArchive(); + $zip->open( $filename, ZipArchive::OVERWRITE ); + $zip->addEmptyDir( 'theme' ); + $zip->addEmptyDir( 'theme/block-templates' ); + $zip->addEmptyDir( 'theme/block-template-parts' ); + +- // Load files into ZIP file. +- foreach ( get_template_types() as $template_type ) { +- // Skip 'embed' for now because it is not a regular template type. +- // Skip 'index' because it's a fallback that we handle differently. +- if ( in_array( $template_type, array( 'embed', 'index' ), true ) ) { +- continue; +- } +- +- $current_template = gutenberg_find_template_post_and_parts( $template_type ); +- if ( isset( $current_template ) ) { +- $zip->addFromString( 'theme/block-templates/' . $current_template['template_post']->post_name . '.html', $current_template['template_post']->post_content ); ++ // Load templates into the zip file. ++ $templates = gutenberg_get_block_templates(); ++ foreach ( $templates as $template ) { ++ $zip->addFromString( ++ 'theme/block-templates/' . $template->slug . '.html', ++ $template->content ++ ); ++ } + +- foreach ( $current_template['template_part_ids'] as $template_part_id ) { +- $template_part = get_post( $template_part_id ); +- $zip->addFromString( 'theme/block-template-parts/' . $template_part->post_name . '.html', $template_part->post_content ); +- } +- } ++ // Load template parts into the zip file. ++ $template_parts = gutenberg_get_block_templates( array(), 'wp_template_part' ); ++ foreach ( $template_parts as $template_part ) { ++ $zip->addFromString( ++ 'theme/block-template-parts/' . $template_part->slug . '.html', ++ $template_part->content ++ ); + } + + // Send back the ZIP file. +@@ -46,6 +48,7 @@ function gutenberg_edit_site_export() { + echo readfile( $filename ); + die(); + } ++ + add_action( + 'rest_api_init', + function () { +diff --git a/lib/edit-site-page.php b/lib/full-site-editing/edit-site-page.php +similarity index 76% +rename from lib/edit-site-page.php +rename to lib/full-site-editing/edit-site-page.php +index 83089981f5..d73ad831a5 100644 +--- a/lib/edit-site-page.php ++++ b/lib/full-site-editing/edit-site-page.php +@@ -87,7 +87,7 @@ function gutenberg_get_editor_styles() { + * @param string $hook Page. + */ + function gutenberg_edit_site_init( $hook ) { +- global $current_screen; ++ global $current_screen, $post; + + if ( ! gutenberg_is_edit_site_page( $hook ) ) { + return; +@@ -101,46 +101,19 @@ function gutenberg_edit_site_init( $hook ) { + */ + $current_screen->is_block_editor( true ); + +- // Get editor settings. +- $max_upload_size = wp_max_upload_size(); +- if ( ! $max_upload_size ) { +- $max_upload_size = 0; +- } +- +- // This filter is documented in wp-admin/includes/media.php. +- $image_size_names = apply_filters( +- 'image_size_names_choose', ++ $settings = array_merge( ++ gutenberg_get_common_block_editor_settings(), + array( +- 'thumbnail' => __( 'Thumbnail', 'gutenberg' ), +- 'medium' => __( 'Medium', 'gutenberg' ), +- 'large' => __( 'Large', 'gutenberg' ), +- 'full' => __( 'Full Size', 'gutenberg' ), ++ 'alignWide' => get_theme_support( 'align-wide' ), ++ 'siteUrl' => site_url(), ++ 'postsPerPage' => get_option( 'posts_per_page' ), ++ 'styles' => gutenberg_get_editor_styles(), ++ 'defaultTemplateTypes' => gutenberg_get_indexed_default_template_types(), ++ '__experimentalBlockPatterns' => WP_Block_Patterns_Registry::get_instance()->get_all_registered(), ++ '__experimentalBlockPatternCategories' => WP_Block_Pattern_Categories_Registry::get_instance()->get_all_registered(), + ) + ); +- $available_image_sizes = array(); +- foreach ( $image_size_names as $image_size_slug => $image_size_name ) { +- $available_image_sizes[] = array( +- 'slug' => $image_size_slug, +- 'name' => $image_size_name, +- ); +- } +- +- $settings = array( +- 'alignWide' => get_theme_support( 'align-wide' ), +- 'imageSizes' => $available_image_sizes, +- 'isRTL' => is_rtl(), +- 'maxUploadFileSize' => $max_upload_size, +- 'siteUrl' => site_url(), +- 'postsPerPage' => get_option( 'posts_per_page' ), +- ); +- +- $settings['styles'] = gutenberg_get_editor_styles(); +- $settings = gutenberg_experimental_global_styles_settings( $settings ); +- +- // This is so other parts of the code can hook their own settings. +- // Example: Global Styles. +- global $post; +- $settings = apply_filters( 'block_editor_settings', $settings, $post ); ++ $settings = gutenberg_experimental_global_styles_settings( $settings ); + + // Preload block editor paths. + // most of these are copied from edit-forms-blocks.php. +@@ -170,7 +143,7 @@ function gutenberg_edit_site_init( $hook ) { + 'wp.domReady( function() { + wp.editSite.initialize( "edit-site-editor", %s ); + } );', +- wp_json_encode( gutenberg_experiments_editor_settings( $settings ) ) ++ wp_json_encode( $settings ) + ) + ); + +@@ -211,7 +184,7 @@ add_action( 'admin_enqueue_scripts', 'gutenberg_edit_site_init' ); + */ + function register_site_editor_homepage_settings() { + register_setting( +- 'general', ++ 'reading', + 'show_on_front', + array( + 'show_in_rest' => true, +@@ -221,7 +194,7 @@ function register_site_editor_homepage_settings() { + ); + + register_setting( +- 'general', ++ 'reading', + 'page_on_front', + array( + 'show_in_rest' => true, +diff --git a/lib/full-site-editing/full-site-editing.php b/lib/full-site-editing/full-site-editing.php +new file mode 100644 +index 0000000000..059b39ccb9 +--- /dev/null ++++ b/lib/full-site-editing/full-site-editing.php +@@ -0,0 +1,123 @@ ++ ++
++

++
++ $menu_item ) { ++ if ( ++ false !== strpos( $menu_item[2], 'customize.php' ) || ++ false !== strpos( $menu_item[2], 'gutenberg-widgets' ) ++ ) { ++ $indexes_to_remove[] = $index; ++ } ++ } ++ ++ foreach ( $indexes_to_remove as $index ) { ++ unset( $submenu['themes.php'][ $index ] ); ++ } ++ } ++} ++ ++add_action( 'admin_menu', 'gutenberg_remove_legacy_pages' ); ++ ++/** ++ * Removes legacy adminbar items from FSE themes. ++ * ++ * @param WP_Admin_Bar $wp_admin_bar The admin-bar instance. ++ */ ++function gutenberg_adminbar_items( $wp_admin_bar ) { ++ ++ // Early exit if not an FSE theme. ++ if ( ! gutenberg_is_fse_theme() ) { ++ return; ++ } ++ ++ // Remove customizer link. ++ $wp_admin_bar->remove_node( 'customize' ); ++ $wp_admin_bar->remove_node( 'customize-background' ); ++ $wp_admin_bar->remove_node( 'customize-header' ); ++ $wp_admin_bar->remove_node( 'widgets' ); ++ ++ // Add site-editor link. ++ if ( ! is_admin() && current_user_can( 'edit_theme_options' ) ) { ++ $wp_admin_bar->add_node( ++ array( ++ 'id' => 'site-editor', ++ 'title' => __( 'Edit site', 'gutenberg' ), ++ 'href' => admin_url( 'admin.php?page=gutenberg-edit-site' ), ++ ) ++ ); ++ } ++} ++ ++add_action( 'admin_bar_menu', 'gutenberg_adminbar_items', 50 ); ++ ++/** ++ * Activates the 'menu_order' filter and then hooks into 'menu_order' ++ */ ++add_filter( 'custom_menu_order', '__return_true' ); ++add_filter( 'menu_order', 'gutenberg_menu_order' ); ++ ++/** ++ * Filters WordPress default menu order ++ * ++ * @param array $menu_order Menu Order. ++ */ ++function gutenberg_menu_order( $menu_order ) { ++ if ( ! gutenberg_is_fse_theme() ) { ++ return $menu_order; ++ } ++ ++ $new_positions = array( ++ // Position the site editor before the appearnce menu. ++ 'gutenberg-edit-site' => array_search( 'themes.php', $menu_order, true ), ++ ); ++ ++ // Traverse through the new positions and move ++ // the items if found in the original menu_positions. ++ foreach ( $new_positions as $value => $new_index ) { ++ $current_index = array_search( $value, $menu_order, true ); ++ if ( $current_index ) { ++ $out = array_splice( $menu_order, $current_index, 1 ); ++ array_splice( $menu_order, $new_index, 0, $out ); ++ } ++ } ++ return $menu_order; ++} +diff --git a/lib/full-site-editing/page-templates.php b/lib/full-site-editing/page-templates.php +new file mode 100644 +index 0000000000..89ce31dba1 +--- /dev/null ++++ b/lib/full-site-editing/page-templates.php +@@ -0,0 +1,42 @@ ++ $page_template ) { ++ if ( ( ! isset( $page_template['postTypes'] ) && 'page' === $post->post_type ) || ++ ( isset( $page_template['postTypes'] ) && in_array( $post->post_type, $page_template['postTypes'], true ) ) ++ ) { ++ $page_templates[ $key ] = $page_template['title']; ++ } ++ } ++ } ++ ++ return $page_templates; ++} ++add_filter( 'theme_post_templates', 'gutenberg_load_block_page_templates', 10, 3 ); ++add_filter( 'theme_page_templates', 'gutenberg_load_block_page_templates', 10, 3 ); +diff --git a/lib/full-site-editing/template-loader.php b/lib/full-site-editing/template-loader.php +new file mode 100644 +index 0000000000..1ffdec27a3 +--- /dev/null ++++ b/lib/full-site-editing/template-loader.php +@@ -0,0 +1,230 @@ ++ get_front_page_template. ++ $template_hierarchy_filter = str_replace( '-', '', $template_type ) . '_template_hierarchy'; // front-page -> frontpage_template_hierarchy. ++ ++ $result = array(); ++ $template_hierarchy_filter_function = function( $templates ) use ( &$result ) { ++ $result = $templates; ++ return $templates; ++ }; ++ ++ add_filter( $template_hierarchy_filter, $template_hierarchy_filter_function, 20, 1 ); ++ call_user_func( $get_template_function ); // This invokes template_hierarchy_filter. ++ remove_filter( $template_hierarchy_filter, $template_hierarchy_filter_function, 20 ); ++ ++ return $result; ++} ++ ++/** ++ * Filters into the "{$type}_template" hooks to redirect them to the Full Site Editing template canvas. ++ * ++ * Internally, this communicates the block content that needs to be used by the template canvas through a global variable. ++ * ++ * @param string $template Path to the template. See locate_template(). ++ * @param string $type Sanitized filename without extension. ++ * @param array $templates A list of template candidates, in descending order of priority. ++ * @return string The path to the Full Site Editing template canvas file. ++ */ ++function gutenberg_override_query_template( $template, $type, array $templates = array() ) { ++ global $_wp_current_template_content; ++ $current_template = gutenberg_resolve_template( $type, $templates ); ++ ++ if ( $current_template ) { ++ $_wp_current_template_content = empty( $current_template->content ) ? __( 'Empty template.', 'gutenberg' ) : $current_template->content; ++ ++ if ( isset( $_GET['_wp-find-template'] ) ) { ++ wp_send_json_success( $current_template ); ++ } ++ } else { ++ if ( 'index' === $type ) { ++ if ( isset( $_GET['_wp-find-template'] ) ) { ++ wp_send_json_error( array( 'message' => __( 'No matching template found.', 'gutenberg' ) ) ); ++ } ++ } else { ++ return false; // So that the template loader keeps looking for templates. ++ } ++ } ++ ++ // Add hooks for template canvas. ++ // Add viewport meta tag. ++ add_action( 'wp_head', 'gutenberg_viewport_meta_tag', 0 ); ++ ++ // Render title tag with content, regardless of whether theme has title-tag support. ++ remove_action( 'wp_head', '_wp_render_title_tag', 1 ); // Remove conditional title tag rendering... ++ add_action( 'wp_head', 'gutenberg_render_title_tag', 1 ); // ...and make it unconditional. ++ ++ // This file will be included instead of the theme's template file. ++ return gutenberg_dir_path() . 'lib/template-canvas.php'; ++} ++ ++/** ++ * Return the correct 'wp_template' to render fot the request template type. ++ * ++ * Accepts an optional $template_hierarchy argument as a hint. ++ * ++ * @param string $template_type The current template type. ++ * @param string[] $template_hierarchy (optional) The current template hierarchy, ordered by priority. ++ * @return null|array { ++ * @type WP_Post|null template_post A template post object, or null if none could be found. ++ * @type int[] A list of template parts IDs for the template. ++ * } ++ */ ++function gutenberg_resolve_template( $template_type, $template_hierarchy = array() ) { ++ if ( ! $template_type ) { ++ return null; ++ } ++ ++ if ( empty( $template_hierarchy ) ) { ++ if ( 'index' === $template_type ) { ++ $template_hierarchy = get_template_hierarchy( 'index' ); ++ } else { ++ $template_hierarchy = array_merge( get_template_hierarchy( $template_type ), get_template_hierarchy( 'index' ) ); ++ } ++ } ++ ++ $slugs = array_map( ++ 'gutenberg_strip_php_suffix', ++ $template_hierarchy ++ ); ++ ++ // Find all potential templates 'wp_template' post matching the hierarchy. ++ $query = array( ++ 'theme' => wp_get_theme()->get_stylesheet(), ++ 'slug__in' => $slugs, ++ ); ++ $templates = gutenberg_get_block_templates( $query ); ++ ++ // Order these templates per slug priority. ++ // Build map of template slugs to their priority in the current hierarchy. ++ $slug_priorities = array_flip( $slugs ); ++ ++ usort( ++ $templates, ++ function ( $template_a, $template_b ) use ( $slug_priorities ) { ++ return $slug_priorities[ $template_a->slug ] - $slug_priorities[ $template_b->slug ]; ++ } ++ ); ++ ++ return count( $templates ) ? $templates[0] : null; ++} ++ ++/** ++ * Displays title tag with content, regardless of whether theme has title-tag support. ++ * ++ * @see _wp_render_title_tag() ++ */ ++function gutenberg_render_title_tag() { ++ echo '' . wp_get_document_title() . '' . "\n"; ++} ++ ++/** ++ * Renders the markup for the current template. ++ */ ++function gutenberg_render_the_template() { ++ global $_wp_current_template_content; ++ global $wp_embed; ++ ++ if ( ! $_wp_current_template_content ) { ++ echo '

' . esc_html__( 'No matching template found', 'gutenberg' ) . '

'; ++ return; ++ } ++ ++ $content = $wp_embed->run_shortcode( $_wp_current_template_content ); ++ $content = $wp_embed->autoembed( $content ); ++ $content = do_blocks( $content ); ++ $content = wptexturize( $content ); ++ if ( function_exists( 'wp_filter_content_tags' ) ) { ++ $content = wp_filter_content_tags( $content ); ++ } else { ++ $content = wp_make_content_images_responsive( $content ); ++ } ++ $content = str_replace( ']]>', ']]>', $content ); ++ ++ // Wrap block template in .wp-site-blocks to allow for specific descendant styles ++ // (e.g. `.wp-site-blocks > *`). ++ echo '
'; ++ echo $content; // phpcs:ignore WordPress.Security.EscapeOutput ++ echo '
'; ++} ++ ++/** ++ * Renders a 'viewport' meta tag. ++ * ++ * This is hooked into {@see 'wp_head'} to decouple its output from the default template canvas. ++ */ ++function gutenberg_viewport_meta_tag() { ++ echo '' . "\n"; ++} ++ ++/** ++ * Strips .php suffix from template file names. ++ * ++ * @access private ++ * ++ * @param string $template_file Template file name. ++ * @return string Template file name without extension. ++ */ ++function gutenberg_strip_php_suffix( $template_file ) { ++ return preg_replace( '/\.(php|html)$/', '', $template_file ); ++} ++ ++/** ++ * Removes post details from block context when rendering a block template. ++ * ++ * @param array $context Default context. ++ * ++ * @return array Filtered context. ++ */ ++function gutenberg_template_render_without_post_block_context( $context ) { ++ /* ++ * When loading a template or template part directly and not through a page ++ * that resolves it, the top-level post ID and type context get set to that ++ * of the template part. Templates are just the structure of a site, and ++ * they should not be available as post context because blocks like Post ++ * Content would recurse infinitely. ++ */ ++ if ( isset( $context['postType'] ) && ++ ( 'wp_template' === $context['postType'] || 'wp_template_part' === $context['postType'] ) ) { ++ unset( $context['postId'] ); ++ unset( $context['postType'] ); ++ } ++ ++ return $context; ++} ++add_filter( 'render_block_context', 'gutenberg_template_render_without_post_block_context' ); +diff --git a/lib/full-site-editing/template-parts.php b/lib/full-site-editing/template-parts.php +new file mode 100644 +index 0000000000..172ca6a5ed +--- /dev/null ++++ b/lib/full-site-editing/template-parts.php +@@ -0,0 +1,120 @@ ++ __( 'Template Parts', 'gutenberg' ), ++ 'singular_name' => __( 'Template Part', 'gutenberg' ), ++ 'menu_name' => _x( 'Template Parts', 'Admin Menu text', 'gutenberg' ), ++ 'add_new' => _x( 'Add New', 'Template Part', 'gutenberg' ), ++ 'add_new_item' => __( 'Add New Template Part', 'gutenberg' ), ++ 'new_item' => __( 'New Template Part', 'gutenberg' ), ++ 'edit_item' => __( 'Edit Template Part', 'gutenberg' ), ++ 'view_item' => __( 'View Template Part', 'gutenberg' ), ++ 'all_items' => __( 'All Template Parts', 'gutenberg' ), ++ 'search_items' => __( 'Search Template Parts', 'gutenberg' ), ++ 'parent_item_colon' => __( 'Parent Template Part:', 'gutenberg' ), ++ 'not_found' => __( 'No template parts found.', 'gutenberg' ), ++ 'not_found_in_trash' => __( 'No template parts found in Trash.', 'gutenberg' ), ++ 'archives' => __( 'Template part archives', 'gutenberg' ), ++ 'insert_into_item' => __( 'Insert into template part', 'gutenberg' ), ++ 'uploaded_to_this_item' => __( 'Uploaded to this template part', 'gutenberg' ), ++ 'filter_items_list' => __( 'Filter template parts list', 'gutenberg' ), ++ 'items_list_navigation' => __( 'Template parts list navigation', 'gutenberg' ), ++ 'items_list' => __( 'Template parts list', 'gutenberg' ), ++ ); ++ ++ $args = array( ++ 'labels' => $labels, ++ 'description' => __( 'Template parts to include in your templates.', 'gutenberg' ), ++ 'public' => false, ++ 'has_archive' => false, ++ 'show_ui' => true, ++ 'show_in_menu' => 'themes.php', ++ 'show_in_admin_bar' => false, ++ 'show_in_rest' => true, ++ 'rest_base' => 'template-parts', ++ 'rest_controller_class' => 'WP_REST_Templates_Controller', ++ 'map_meta_cap' => true, ++ 'supports' => array( ++ 'title', ++ 'slug', ++ 'excerpt', ++ 'editor', ++ 'revisions', ++ ), ++ ); ++ ++ register_post_type( 'wp_template_part', $args ); ++} ++add_action( 'init', 'gutenberg_register_template_part_post_type' ); ++ ++/** ++ * Fixes the label of the 'wp_template_part' admin menu entry. ++ */ ++function gutenberg_fix_template_part_admin_menu_entry() { ++ if ( ! gutenberg_is_fse_theme() ) { ++ return; ++ } ++ ++ global $submenu; ++ if ( ! isset( $submenu['themes.php'] ) ) { ++ return; ++ } ++ $post_type = get_post_type_object( 'wp_template_part' ); ++ if ( ! $post_type ) { ++ return; ++ } ++ foreach ( $submenu['themes.php'] as $key => $submenu_entry ) { ++ if ( $post_type->labels->all_items === $submenu['themes.php'][ $key ][0] ) { ++ $submenu['themes.php'][ $key ][0] = $post_type->labels->menu_name; // phpcs:ignore WordPress.WP.GlobalVariablesOverride ++ break; ++ } ++ } ++} ++add_action( 'admin_menu', 'gutenberg_fix_template_part_admin_menu_entry' ); ++ ++// Customize the `wp_template` admin list. ++add_filter( 'manage_wp_template_part_posts_columns', 'gutenberg_templates_lists_custom_columns' ); ++add_action( 'manage_wp_template_part_posts_custom_column', 'gutenberg_render_templates_lists_custom_column', 10, 2 ); ++add_filter( 'views_edit-wp_template_part', 'gutenberg_filter_templates_edit_views' ); ++ ++/** ++ * Sets a custom slug when creating auto-draft template parts. ++ * This is only needed for auto-drafts created by the regular WP editor. ++ * If this page is to be removed, this won't be necessary. ++ * ++ * @param int $post_id Post ID. ++ */ ++function set_unique_slug_on_create_template_part( $post_id ) { ++ $post = get_post( $post_id ); ++ if ( 'auto-draft' !== $post->post_status ) { ++ return; ++ } ++ ++ if ( ! $post->post_name ) { ++ wp_update_post( ++ array( ++ 'ID' => $post_id, ++ 'post_name' => 'custom_slug_' . uniqid(), ++ ) ++ ); ++ } ++ ++ $terms = get_the_terms( $post_id, 'wp_theme' ); ++ if ( ! $terms || ! count( $terms ) ) { ++ wp_set_post_terms( $post_id, wp_get_theme()->get_stylesheet(), 'wp_theme' ); ++ } ++} ++add_action( 'save_post_wp_template_part', 'set_unique_slug_on_create_template_part' ); +diff --git a/lib/full-site-editing/templates-utils.php b/lib/full-site-editing/templates-utils.php +new file mode 100644 +index 0000000000..bc270a738d +--- /dev/null ++++ b/lib/full-site-editing/templates-utils.php +@@ -0,0 +1,113 @@ ++post_name ); ++ return; ++ } ++ ++ if ( 'description' === $column_name && has_excerpt( $post_id ) ) { ++ the_excerpt( $post_id ); ++ return; ++ } ++ ++ if ( 'status' === $column_name ) { ++ $post_status = get_post_status( $post_id ); ++ // The auto-draft status doesn't have localized labels. ++ if ( 'auto-draft' === $post_status ) { ++ echo esc_html_x( 'Auto-Draft', 'Post status', 'gutenberg' ); ++ return; ++ } ++ $post_status_object = get_post_status_object( $post_status ); ++ echo esc_html( $post_status_object->label ); ++ return; ++ } ++ ++ if ( 'theme' === $column_name ) { ++ $terms = get_the_terms( $post_id, 'wp_theme' ); ++ if ( empty( $terms ) || is_wp_error( $terms ) ) { ++ return; ++ } ++ $themes = array(); ++ foreach ( $terms as $term ) { ++ $themes[] = esc_html( wp_get_theme( $term->slug ) ); ++ } ++ echo implode( '
', $themes ); ++ return; ++ } ++} ++ ++/** ++ * Adds the auto-draft view to the templates and template parts admin lists. ++ * ++ * @param array $views The edit views to filter. ++ */ ++function gutenberg_filter_templates_edit_views( $views ) { ++ $post_type = get_current_screen()->post_type; ++ $url = add_query_arg( ++ array( ++ 'post_type' => $post_type, ++ 'post_status' => 'auto-draft', ++ ), ++ 'edit.php' ++ ); ++ $is_auto_draft_view = isset( $_REQUEST['post_status'] ) && 'auto-draft' === $_REQUEST['post_status']; ++ $class_html = $is_auto_draft_view ? ' class="current"' : ''; ++ $aria_current = $is_auto_draft_view ? ' aria-current="page"' : ''; ++ $post_count = wp_count_posts( $post_type, 'readable' ); ++ $label = sprintf( ++ // The auto-draft status doesn't have localized labels. ++ translate_nooped_plural( ++ /* translators: %s: Number of auto-draft posts. */ ++ _nx_noop( ++ 'Auto-Draft (%s)', ++ 'Auto-Drafts (%s)', ++ 'Post status', ++ 'gutenberg' ++ ), ++ $post_count->{'auto-draft'} ++ ), ++ number_format_i18n( $post_count->{'auto-draft'} ) ++ ); ++ ++ $auto_draft_view = sprintf( ++ '%s', ++ esc_url( $url ), ++ $class_html, ++ $aria_current, ++ $label ++ ); ++ ++ array_splice( $views, 1, 0, array( 'auto-draft' => $auto_draft_view ) ); ++ ++ return $views; ++} +diff --git a/lib/templates.php b/lib/full-site-editing/templates.php +similarity index 50% +rename from lib/templates.php +rename to lib/full-site-editing/templates.php +index d0c9075b8d..6362d7716c 100644 +--- a/lib/templates.php ++++ b/lib/full-site-editing/templates.php +@@ -21,12 +21,6 @@ function gutenberg_get_template_paths() { + $block_template_files = array_merge( $block_template_files, $child_block_template_files ); + } + +- if ( gutenberg_is_experiment_enabled( 'gutenberg-full-site-editing-demo' ) ) { +- $demo_block_template_files = glob( dirname( __FILE__ ) . '/demo-block-templates/*.html' ); +- $demo_block_template_files = is_array( $demo_block_template_files ) ? $demo_block_template_files : array(); +- $block_template_files = array_merge( $block_template_files, $demo_block_template_files ); +- } +- + return $block_template_files; + } + +@@ -34,6 +28,10 @@ function gutenberg_get_template_paths() { + * Registers block editor 'wp_template' post type. + */ + function gutenberg_register_template_post_type() { ++ if ( ! gutenberg_is_fse_theme() ) { ++ return; ++ } ++ + $labels = array( + 'name' => __( 'Templates', 'gutenberg' ), + 'singular_name' => __( 'Template', 'gutenberg' ), +@@ -57,20 +55,22 @@ function gutenberg_register_template_post_type() { + ); + + $args = array( +- 'labels' => $labels, +- 'description' => __( 'Templates to include in your theme.', 'gutenberg' ), +- 'public' => false, +- 'has_archive' => false, +- 'show_ui' => true, +- 'show_in_menu' => 'themes.php', +- 'show_in_admin_bar' => false, +- 'show_in_rest' => true, +- 'rest_base' => 'templates', +- 'capability_type' => array( 'template', 'templates' ), +- 'map_meta_cap' => true, +- 'supports' => array( ++ 'labels' => $labels, ++ 'description' => __( 'Templates to include in your theme.', 'gutenberg' ), ++ 'public' => false, ++ 'has_archive' => false, ++ 'show_ui' => true, ++ 'show_in_menu' => 'themes.php', ++ 'show_in_admin_bar' => false, ++ 'show_in_rest' => true, ++ 'rest_base' => 'templates', ++ 'rest_controller_class' => 'WP_REST_Templates_Controller', ++ 'capability_type' => array( 'template', 'templates' ), ++ 'map_meta_cap' => true, ++ 'supports' => array( + 'title', + 'slug', ++ 'excerpt', + 'editor', + 'revisions', + ), +@@ -80,6 +80,35 @@ function gutenberg_register_template_post_type() { + } + add_action( 'init', 'gutenberg_register_template_post_type' ); + ++/** ++ * Registers block editor 'wp_theme' taxonomy. ++ */ ++function gutenberg_register_wp_theme_taxonomy() { ++ if ( ! gutenberg_is_fse_theme() ) { ++ return; ++ } ++ ++ register_taxonomy( ++ 'wp_theme', ++ array( 'wp_template', 'wp_template_part' ), ++ array( ++ 'public' => false, ++ 'hierarchical' => false, ++ 'labels' => array( ++ 'name' => __( 'Themes', 'gutenberg' ), ++ 'singular_name' => __( 'Theme', 'gutenberg' ), ++ ), ++ 'query_var' => false, ++ 'rewrite' => false, ++ 'show_ui' => false, ++ '_builtin' => true, ++ 'show_in_nav_menus' => false, ++ 'show_in_rest' => false, ++ ) ++ ); ++} ++add_action( 'init', 'gutenberg_register_wp_theme_taxonomy' ); ++ + /** + * Filters the capabilities of a user to conditionally grant them capabilities for managing 'wp_template' posts. + * +@@ -106,30 +135,13 @@ function gutenberg_grant_template_caps( array $allcaps ) { + } + add_filter( 'user_has_cap', 'gutenberg_grant_template_caps' ); + +-/** +- * Filters `wp_template` posts slug resolution to bypass deduplication logic as +- * template slugs should be unique. +- * +- * @param string $slug The resolved slug (post_name). +- * @param int $post_ID Post ID. +- * @param string $post_status No uniqueness checks are made if the post is still draft or pending. +- * @param string $post_type Post type. +- * @param int $post_parent Post parent ID. +- * @param int $original_slug The desired slug (post_name). +- * @return string The original, desired slug. +- */ +-function gutenberg_filter_wp_template_wp_unique_post_slug( $slug, $post_ID, $post_status, $post_type, $post_parent, $original_slug ) { +- if ( 'wp_template' === $post_type ) { +- return $original_slug; +- } +- return $slug; +-} +-add_filter( 'wp_unique_post_slug', 'gutenberg_filter_wp_template_wp_unique_post_slug', 10, 6 ); +- + /** + * Fixes the label of the 'wp_template' admin menu entry. + */ + function gutenberg_fix_template_admin_menu_entry() { ++ if ( ! gutenberg_is_fse_theme() ) { ++ return; ++ } + global $submenu; + if ( ! isset( $submenu['themes.php'] ) ) { + return; +@@ -147,87 +159,36 @@ function gutenberg_fix_template_admin_menu_entry() { + } + add_action( 'admin_menu', 'gutenberg_fix_template_admin_menu_entry' ); + +-/** +- * Filters the 'wp_template' post type columns in the admin list table. +- * +- * @param array $columns Columns to display. +- * @return array Filtered $columns. +- */ +-function gutenberg_filter_template_list_table_columns( array $columns ) { +- $columns['slug'] = __( 'Slug', 'gutenberg' ); +- if ( isset( $columns['date'] ) ) { +- unset( $columns['date'] ); +- } +- return $columns; +-} +-add_filter( 'manage_wp_template_posts_columns', 'gutenberg_filter_template_list_table_columns' ); ++// Customize the `wp_template` admin list. ++add_filter( 'manage_wp_template_posts_columns', 'gutenberg_templates_lists_custom_columns' ); ++add_action( 'manage_wp_template_posts_custom_column', 'gutenberg_render_templates_lists_custom_column', 10, 2 ); ++add_filter( 'views_edit-wp_template', 'gutenberg_filter_templates_edit_views' ); + + /** +- * Renders column content for the 'wp_template' post type list table. ++ * Sets a custom slug when creating auto-draft templates. ++ * This is only needed for auto-drafts created by the regular WP editor. ++ * If this page is to be removed, this won't be necessary. + * +- * @param string $column_name Column name to render. +- * @param int $post_id Post ID. ++ * @param int $post_id Post ID. + */ +-function gutenberg_render_template_list_table_column( $column_name, $post_id ) { +- if ( 'slug' !== $column_name ) { ++function set_unique_slug_on_create_template( $post_id ) { ++ $post = get_post( $post_id ); ++ if ( 'auto-draft' !== $post->post_status ) { + return; + } +- $post = get_post( $post_id ); +- echo esc_html( $post->post_name ); +-} +-add_action( 'manage_wp_template_posts_custom_column', 'gutenberg_render_template_list_table_column', 10, 2 ); + +-/** +- * Filter for adding a `resolved` parameter to `wp_template` queries. +- * +- * @param array $query_params The query parameters. +- * @return array Filtered $query_params. +- */ +-function filter_rest_wp_template_collection_params( $query_params ) { +- $query_params += array( +- 'resolved' => array( +- 'description' => __( 'Whether to filter for resolved templates', 'gutenberg' ), +- 'type' => 'boolean', +- ), +- ); +- return $query_params; +-} +-apply_filters( 'rest_wp_template_collection_params', 'filter_rest_wp_template_collection_params', 99, 1 ); +- +-/** +- * Filter for supporting the `resolved` parameter in `wp_template` queries. +- * +- * @param array $args The query arguments. +- * @param WP_REST_Request $request The request object. +- * @return array Filtered $args. +- */ +-function filter_rest_wp_template_query( $args, $request ) { +- // Create auto-drafts for each theme template files. +- $block_template_files = gutenberg_get_template_paths(); +- foreach ( $block_template_files as $path ) { +- $template_type = basename( $path, '.html' ); +- gutenberg_find_template_post_and_parts( $template_type, array( $template_type ) ); ++ if ( ! $post->post_name ) { ++ wp_update_post( ++ array( ++ 'ID' => $post_id, ++ 'post_name' => 'custom_slug_' . uniqid(), ++ ) ++ ); + } + +- if ( $request['resolved'] ) { +- $template_ids = array( 0 ); // Return nothing by default (the 0 is needed for `post__in`). +- $template_types = $request['slug'] ? $request['slug'] : get_template_types(); +- +- foreach ( $template_types as $template_type ) { +- // Skip 'embed' for now because it is not a regular template type. +- if ( in_array( $template_type, array( 'embed' ), true ) ) { +- continue; +- } +- +- $current_template = gutenberg_find_template_post_and_parts( $template_type ); +- if ( isset( $current_template ) ) { +- $template_ids[] = $current_template['template_post']->ID; +- } +- } +- $args['post__in'] = $template_ids; +- $args['post_status'] = array( 'publish', 'auto-draft' ); ++ $terms = get_the_terms( $post_id, 'wp_theme' ); ++ if ( ! $terms || ! count( $terms ) ) { ++ wp_set_post_terms( $post_id, wp_get_theme()->get_stylesheet(), 'wp_theme' ); + } +- +- return $args; + } +-add_filter( 'rest_wp_template_query', 'filter_rest_wp_template_query', 99, 2 ); ++add_action( 'save_post_wp_template', 'set_unique_slug_on_create_template' ); +diff --git a/lib/global-styles.php b/lib/global-styles.php +index 2b4173f58d..5aab927771 100644 +--- a/lib/global-styles.php ++++ b/lib/global-styles.php +@@ -14,698 +14,132 @@ function gutenberg_experimental_global_styles_has_theme_json_support() { + return is_readable( locate_template( 'experimental-theme.json' ) ); + } + +-/** +- * Given a tree, it creates a flattened one +- * by merging the keys and binding the leaf values +- * to the new keys. +- * +- * It also transforms camelCase names into kebab-case +- * and substitutes '/' by '-'. +- * +- * This is thought to be useful to generate +- * CSS Custom Properties from a tree, +- * although there's nothing in the implementation +- * of this function that requires that format. +- * +- * For example, assuming the given prefix is '--wp' +- * and the token is '--', for this input tree: +- * +- * { +- * 'some/property': 'value', +- * 'nestedProperty': { +- * 'sub-property': 'value' +- * } +- * } +- * +- * it'll return this output: +- * +- * { +- * '--wp--some-property': 'value', +- * '--wp--nested-property--sub-property': 'value' +- * } +- * +- * @param array $tree Input tree to process. +- * @param string $prefix Prefix to prepend to each variable. '' by default. +- * @param string $token Token to use between levels. '--' by default. +- * +- * @return array The flattened tree. +- */ +-function gutenberg_experimental_global_styles_get_css_vars( $tree, $prefix = '', $token = '--' ) { +- $result = array(); +- foreach ( $tree as $property => $value ) { +- $new_key = $prefix . str_replace( +- '/', +- '-', +- strtolower( preg_replace( '/(?get_stylesheet() ); +- $recent_posts = wp_get_recent_posts( +- array( +- 'numberposts' => 1, +- 'orderby' => 'date', +- 'order' => 'desc', +- 'post_type' => $post_type_filter, +- 'post_status' => $post_status_filter, +- 'name' => $post_name_filter, +- ) +- ); +- +- if ( is_array( $recent_posts ) && ( count( $recent_posts ) === 1 ) ) { +- $user_cpt = $recent_posts[0]; +- } elseif ( $should_create_cpt ) { +- $cpt_post_id = wp_insert_post( +- array( +- 'post_content' => '{}', +- 'post_status' => 'publish', +- 'post_type' => $post_type_filter, +- 'post_name' => $post_name_filter, +- ), +- true +- ); +- $user_cpt = get_post( $cpt_post_id, ARRAY_A ); +- } +- +- return $user_cpt; +-} +- +-/** +- * Returns the post ID of the CPT containing the user's origin config. +- * +- * @return integer +- */ +-function gutenberg_experimental_global_styles_get_user_cpt_id() { +- $user_cpt_id = null; +- $user_cpt = gutenberg_experimental_global_styles_get_user_cpt( true ); +- if ( array_key_exists( 'ID', $user_cpt ) ) { +- $user_cpt_id = $user_cpt['ID']; +- } +- return $user_cpt_id; +-} +- +-/** +- * Return core's origin config. +- * +- * @return array Config that adheres to the theme.json schema. +- */ +-function gutenberg_experimental_global_styles_get_core() { +- $config = gutenberg_experimental_global_styles_get_from_file( +- __DIR__ . '/experimental-default-theme.json' +- ); +- // Start i18n logic to remove when JSON i18 strings are extracted. +- $default_colors_i18n = array( +- 'black' => __( 'Black', 'gutenberg' ), +- 'cyan-bluish-gray' => __( 'Cyan bluish gray', 'gutenberg' ), +- 'white' => __( 'White', 'gutenberg' ), +- 'pale-pink' => __( 'Pale pink', 'gutenberg' ), +- 'vivid-red' => __( 'Vivid red', 'gutenberg' ), +- 'luminous-vivid-orange' => __( 'Luminous vivid orange', 'gutenberg' ), +- 'luminous-vivid-amber' => __( 'Luminous vivid amber', 'gutenberg' ), +- 'light-green-cyan' => __( 'Light green cyan', 'gutenberg' ), +- 'vivid-green-cyan' => __( 'Vivid green cyan', 'gutenberg' ), +- 'pale-cyan-blue' => __( 'Pale cyan blue', 'gutenberg' ), +- 'vivid-cyan-blue' => __( 'Vivid cyan blue', 'gutenberg' ), +- 'vivid-purple' => __( 'Vivid purple', 'gutenberg' ), +- ); +- +- if ( ! empty( $config['global']['settings']['color']['palette'] ) ) { +- foreach ( $config['global']['settings']['color']['palette'] as &$color ) { +- $color['name'] = $default_colors_i18n[ $color['slug'] ]; +- } +- } +- +- $default_gradients_i18n = array( +- 'vivid-cyan-blue-to-vivid-purple' => __( 'Vivid cyan blue to vivid purple', 'gutenberg' ), +- 'light-green-cyan-to-vivid-green-cyan' => __( 'Light green cyan to vivid green cyan', 'gutenberg' ), +- 'luminous-vivid-amber-to-luminous-vivid-orange' => __( 'Luminous vivid amber to luminous vivid orange', 'gutenberg' ), +- 'luminous-vivid-orange-to-vivid-red' => __( 'Luminous vivid orange to vivid red', 'gutenberg' ), +- 'very-light-gray-to-cyan-bluish-gray' => __( 'Very light gray to cyan bluish gray', 'gutenberg' ), +- 'cool-to-warm-spectrum' => __( 'Cool to warm spectrum', 'gutenberg' ), +- 'blush-light-purple' => __( 'Blush light purple', 'gutenberg' ), +- 'blush-bordeaux' => __( 'Blush bordeaux', 'gutenberg' ), +- 'luminous-dusk' => __( 'Luminous dusk', 'gutenberg' ), +- 'pale-ocean' => __( 'Pale ocean', 'gutenberg' ), +- 'electric-grass' => __( 'Electric grass', 'gutenberg' ), +- 'midnight' => __( 'Midnight', 'gutenberg' ), +- ); +- +- if ( ! empty( $config['global']['settings']['color']['gradients'] ) ) { +- foreach ( $config['global']['settings']['color']['gradients'] as &$gradient ) { +- $gradient['name'] = $default_gradients_i18n[ $gradient['slug'] ]; +- } +- } +- +- $default_font_sizes_i18n = array( +- 'small' => __( 'Small', 'gutenberg' ), +- 'normal' => __( 'Normal', 'gutenberg' ), +- 'medium' => __( 'Medium', 'gutenberg' ), +- 'large' => __( 'Large', 'gutenberg' ), +- 'huge' => __( 'Huge', 'gutenberg' ), +- ); +- +- if ( ! empty( $config['global']['settings']['typography']['fontSizes'] ) ) { +- foreach ( $config['global']['settings']['typography']['fontSizes'] as &$font_size ) { +- $font_size['name'] = $default_font_sizes_i18n[ $font_size['slug'] ]; +- } +- } +- // End i18n logic to remove when JSON i18 strings are extracted. +- return $config; +-} +- + /** + * Returns the theme presets registered via add_theme_support, if any. + * ++ * @param array $settings Existing editor settings. ++ * + * @return array Config that adheres to the theme.json schema. + */ +-function gutenberg_experimental_global_styles_get_theme_support_settings() { ++function gutenberg_experimental_global_styles_get_theme_support_settings( $settings ) { + $theme_settings = array(); + $theme_settings['global'] = array(); + $theme_settings['global']['settings'] = array(); + + // Deprecated theme supports. +- if ( get_theme_support( 'disable-custom-colors' ) ) { ++ if ( isset( $settings['disableCustomColors'] ) ) { + if ( ! isset( $theme_settings['global']['settings']['color'] ) ) { + $theme_settings['global']['settings']['color'] = array(); + } +- $theme_settings['global']['settings']['color']['custom'] = false; ++ $theme_settings['global']['settings']['color']['custom'] = ! $settings['disableCustomColors']; + } +- if ( get_theme_support( 'disable-custom-gradients' ) ) { ++ ++ if ( isset( $settings['disableCustomGradients'] ) ) { + if ( ! isset( $theme_settings['global']['settings']['color'] ) ) { + $theme_settings['global']['settings']['color'] = array(); + } +- $theme_settings['global']['settings']['color']['customGradient'] = false; ++ $theme_settings['global']['settings']['color']['customGradient'] = ! $settings['disableCustomGradients']; + } +- if ( get_theme_support( 'disable-custom-font-sizes' ) ) { ++ ++ if ( isset( $settings['disableCustomFontSizes'] ) ) { + if ( ! isset( $theme_settings['global']['settings']['typography'] ) ) { + $theme_settings['global']['settings']['typography'] = array(); + } +- $theme_settings['global']['settings']['typography']['customFontSize'] = false; ++ $theme_settings['global']['settings']['typography']['customFontSize'] = ! $settings['disableCustomFontSizes']; + } +- if ( get_theme_support( 'custom-line-height' ) ) { ++ ++ if ( isset( $settings['enableCustomLineHeight'] ) ) { + if ( ! isset( $theme_settings['global']['settings']['typography'] ) ) { + $theme_settings['global']['settings']['typography'] = array(); + } +- $theme_settings['global']['settings']['typography']['customLineHeight'] = true; +- } +- if ( get_theme_support( 'custom-spacing' ) ) { +- if ( ! isset( $theme_settings['global']['settings']['spacing'] ) ) { +- $theme_settings['global']['settings']['spacing'] = array(); +- } +- $theme_settings['global']['settings']['spacing']['custom'] = true; +- } +- if ( get_theme_support( 'experimental-link-color' ) ) { +- if ( ! isset( $theme_settings['global']['settings']['color'] ) ) { +- $theme_settings['global']['settings']['color'] = array(); +- } +- $theme_settings['global']['settings']['color']['link'] = true; ++ $theme_settings['global']['settings']['typography']['customLineHeight'] = $settings['enableCustomLineHeight']; + } + +- $custom_units_theme_support = get_theme_support( 'custom-units' ); +- if ( $custom_units_theme_support ) { ++ if ( isset( $settings['enableCustomUnits'] ) ) { + if ( ! isset( $theme_settings['global']['settings']['spacing'] ) ) { + $theme_settings['global']['settings']['spacing'] = array(); + } +- $theme_settings['global']['settings']['spacing'] ['units'] = true === $custom_units_theme_support ? array( 'px', 'em', 'rem', 'vh', 'vw' ) : $custom_units_theme_support; ++ $theme_settings['global']['settings']['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ? ++ array( 'px', 'em', 'rem', 'vh', 'vw' ) : ++ $settings['enableCustomUnits']; + } + +- $theme_colors = get_theme_support( 'editor-color-palette' ); +- if ( ! empty( $theme_colors[0] ) ) { ++ if ( isset( $settings['colors'] ) ) { + if ( ! isset( $theme_settings['global']['settings']['color'] ) ) { + $theme_settings['global']['settings']['color'] = array(); + } +- $theme_settings['global']['settings']['color']['palette'] = array(); +- $theme_settings['global']['settings']['color']['palette'] = $theme_colors[0]; ++ $theme_settings['global']['settings']['color']['palette'] = $settings['colors']; + } + +- $theme_gradients = get_theme_support( 'editor-gradient-presets' ); +- if ( ! empty( $theme_gradients[0] ) ) { ++ if ( isset( $settings['gradients'] ) ) { + if ( ! isset( $theme_settings['global']['settings']['color'] ) ) { + $theme_settings['global']['settings']['color'] = array(); + } +- $theme_settings['global']['settings']['color']['gradients'] = array(); +- $theme_settings['global']['settings']['color']['gradients'] = $theme_gradients[0]; ++ $theme_settings['global']['settings']['color']['gradients'] = $settings['gradients']; + } + +- $theme_font_sizes = get_theme_support( 'editor-font-sizes' ); +- if ( ! empty( $theme_font_sizes[0] ) ) { ++ if ( isset( $settings['fontSizes'] ) ) { ++ $font_sizes = $settings['fontSizes']; ++ // Back-compatibility for presets without units. ++ foreach ( $font_sizes as &$font_size ) { ++ if ( is_numeric( $font_size['size'] ) ) { ++ $font_size['size'] = $font_size['size'] . 'px'; ++ } ++ } + if ( ! isset( $theme_settings['global']['settings']['typography'] ) ) { + $theme_settings['global']['settings']['typography'] = array(); + } +- $theme_settings['global']['settings']['typography']['fontSizes'] = array(); +- $theme_settings['global']['settings']['typography']['fontSizes'] = $theme_font_sizes[0]; +- } +- +- return $theme_settings; +-} +- +-/** +- * Returns the theme's origin config. +- * +- * It also fetches the existing presets the theme declared via add_theme_support +- * and uses them if the theme hasn't declared any via theme.json. +- * +- * @return array Config that adheres to the theme.json schema. +- */ +-function gutenberg_experimental_global_styles_get_theme() { +- $theme_support_settings = gutenberg_experimental_global_styles_get_theme_support_settings(); +- $theme_config = gutenberg_experimental_global_styles_get_from_file( +- locate_template( 'experimental-theme.json' ) +- ); +- +- /* +- * We want the presets declared in theme.json +- * to take precedence over the ones declared via add_theme_support. +- * +- * Note that merging happens at the preset category level. Example: +- * +- * - if the theme declares a color palette via add_theme_support & +- * a set of font sizes via theme.json, both will be included in the output. +- * +- * - if the theme declares a color palette both via add_theme_support & +- * via theme.json, the later takes precedence. +- * +- */ +- $theme_config = gutenberg_experimental_global_styles_merge_trees( +- $theme_support_settings, +- $theme_config +- ); +- +- return $theme_config; +-} +- +-/** +- * Convert style property to its CSS name. +- * +- * @param string $style_property Style property name. +- * @return string CSS property name. +- */ +-function gutenberg_experimental_global_styles_get_css_property( $style_property ) { +- switch ( $style_property ) { +- case 'backgroundColor': +- return 'background-color'; +- case 'fontSize': +- return 'font-size'; +- case 'lineHeight': +- return 'line-height'; +- default: +- return $style_property; ++ $theme_settings['global']['settings']['typography']['fontSizes'] = $font_sizes; + } +-} + +-/** +- * Return how the style property is structured. +- * +- * @return array Style property structure. +- */ +-function gutenberg_experimental_global_styles_get_style_property() { +- return array( +- '--wp--style--color--link' => array( 'color', 'link' ), +- 'background' => array( 'color', 'gradient' ), +- 'backgroundColor' => array( 'color', 'background' ), +- 'color' => array( 'color', 'text' ), +- 'fontSize' => array( 'typography', 'fontSize' ), +- 'lineHeight' => array( 'typography', 'lineHeight' ), +- ); +-} +- +-/** +- * Return how the support keys are structured. +- * +- * @return array Support keys structure. +- */ +-function gutenberg_experimental_global_styles_get_support_keys() { +- return array( +- '--wp--style--color--link' => array( 'color', 'linkColor' ), +- 'background' => array( 'color', 'gradients' ), +- 'backgroundColor' => array( 'color' ), +- 'color' => array( 'color' ), +- 'fontSize' => array( 'fontSize' ), +- 'lineHeight' => array( 'lineHeight' ), +- ); +-} +- +-/** +- * Returns how the presets css variables are structured on the global styles data. +- * +- * @return array Presets structure +- */ +-function gutenberg_experimental_global_styles_get_presets_structure() { +- return array( +- 'color' => array( +- 'path' => array( 'color', 'palette' ), +- 'key' => 'color', +- ), +- 'gradient' => array( +- 'path' => array( 'color', 'gradients' ), +- 'key' => 'gradient', +- ), +- 'fontSize' => array( +- 'path' => array( 'typography', 'fontSizes' ), +- 'key' => 'size', +- ), +- ); +-} +- +-/** +- * Returns the style features a particular block supports. +- * +- * @param array $supports The block supports array. +- * +- * @return array Style features supported by the block. +- */ +-function gutenberg_experimental_global_styles_get_supported_styles( $supports ) { +- $support_keys = gutenberg_experimental_global_styles_get_support_keys(); +- $supported_features = array(); +- foreach ( $support_keys as $key => $path ) { +- if ( gutenberg_experimental_get( $supports, $path ) ) { +- $supported_features[] = $key; +- } +- } +- +- return $supported_features; +-} +- +-/** +- * Retrieves the block data (selector/supports). +- * +- * @return array +- */ +-function gutenberg_experimental_global_styles_get_block_data() { +- $block_data = array(); +- +- $registry = WP_Block_Type_Registry::get_instance(); +- $blocks = array_merge( +- $registry->get_all_registered(), +- array( +- 'global' => new WP_Block_Type( +- 'global', +- array( +- 'supports' => array( +- '__experimentalSelector' => ':root', +- 'fontSize' => true, +- 'color' => array( +- 'linkColor' => true, +- 'gradients' => true, +- ), +- ), +- ) +- ), +- ) +- ); +- foreach ( $blocks as $block_name => $block_type ) { +- if ( ! property_exists( $block_type, 'supports' ) || empty( $block_type->supports ) || ! is_array( $block_type->supports ) ) { +- continue; +- } +- +- $supports = gutenberg_experimental_global_styles_get_supported_styles( $block_type->supports ); +- +- /* +- * Assign the selector for the block. +- * +- * Some blocks can declare multiple selectors: +- * +- * - core/heading represents the H1-H6 HTML elements +- * - core/list represents the UL and OL HTML elements +- * - core/group is meant to represent DIV and other HTML elements +- * +- * Some other blocks don't provide a selector, +- * so we generate a class for them based on their name: +- * +- * - 'core/group' => '.wp-block-group' +- * - 'my-custom-library/block-name' => '.wp-block-my-custom-library-block-name' +- * +- * Note that, for core blocks, we don't add the `core/` prefix to its class name. +- * This is for historical reasons, as they come with a class without that infix. +- * +- */ +- if ( +- isset( $block_type->supports['__experimentalSelector'] ) && +- is_string( $block_type->supports['__experimentalSelector'] ) +- ) { +- $block_data[ $block_name ] = array( +- 'selector' => $block_type->supports['__experimentalSelector'], +- 'supports' => $supports, +- 'blockName' => $block_name, +- ); +- } elseif ( +- isset( $block_type->supports['__experimentalSelector'] ) && +- is_array( $block_type->supports['__experimentalSelector'] ) +- ) { +- foreach ( $block_type->supports['__experimentalSelector'] as $key => $selector ) { +- $block_data[ $key ] = array( +- 'selector' => $selector, +- 'supports' => $supports, +- 'blockName' => $block_name, +- ); +- } +- } else { +- $block_data[ $block_name ] = array( +- 'selector' => '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) ), +- 'supports' => $supports, +- 'blockName' => $block_name, +- ); ++ // Things that didn't land in core yet, so didn't have a setting assigned. ++ if ( current( (array) get_theme_support( 'custom-spacing' ) ) ) { ++ if ( ! isset( $theme_settings['global']['settings']['spacing'] ) ) { ++ $theme_settings['global']['settings']['spacing'] = array(); + } ++ $theme_settings['global']['settings']['spacing']['customPadding'] = true; + } + +- return $block_data; +-} +- +-/** +- * Given an array contain the styles shape returns the css for this styles. +- * A similar function exists on the client at /packages/block-editor/src/hooks/style.js. +- * +- * @param array $styles Array containing the styles shape from global styles. +- * +- * @return array Containing a set of css rules. +- */ +-function gutenberg_experimental_global_styles_flatten_styles_tree( $styles ) { +- $mappings = gutenberg_experimental_global_styles_get_style_property(); +- +- $result = array(); +- foreach ( $mappings as $key => $path ) { +- $value = gutenberg_experimental_get( $styles, $path, null ); +- if ( null !== $value ) { +- $result[ $key ] = $value; ++ if ( current( (array) get_theme_support( 'experimental-link-color' ) ) ) { ++ if ( ! isset( $theme_settings['global']['settings']['color'] ) ) { ++ $theme_settings['global']['settings']['color'] = array(); + } ++ $theme_settings['global']['settings']['color']['link'] = true; + } +- return $result; +- +-} +- +-/** +- * Given a selector for a block, and the settings of the block, returns a string +- * with the stylesheet of the preset classes required for that block. +- * +- * @param string $selector String with the CSS selector for the block. +- * @param array $settings Array containing the settings of the block. +- * +- * @return string Stylesheet with the preset classes. +- */ +-function gutenberg_experimental_global_styles_get_preset_classes( $selector, $settings ) { +- if ( empty( $settings ) || empty( $selector ) ) { +- return ''; +- } +- +- $stylesheet = ''; +- $class_prefix = 'has'; +- $classes_structure = array( +- 'color' => array( +- 'path' => array( 'color', 'palette' ), +- 'key' => 'color', +- 'property' => 'color', +- ), +- 'background-color' => array( +- 'path' => array( 'color', 'palette' ), +- 'key' => 'color', +- 'property' => 'background-color', +- ), +- 'gradient-background' => array( +- 'path' => array( 'color', 'gradients' ), +- 'key' => 'gradient', +- 'property' => 'background', +- ), +- 'font-size' => array( +- 'path' => array( 'typography', 'fontSizes' ), +- 'key' => 'size', +- 'property' => 'font-size', +- ), +- ); +- +- foreach ( $classes_structure as $class_suffix => $preset_structure ) { +- $path = $preset_structure['path']; +- $presets = gutenberg_experimental_get( $settings, $path ); +- +- if ( empty( $presets ) ) { +- continue; +- } + +- $key = $preset_structure['key']; +- $property = $preset_structure['property']; +- +- foreach ( $presets as $preset ) { +- $slug = $preset['slug']; +- $value = $preset[ $key ]; +- +- $class_to_use = ".$class_prefix-$slug-$class_suffix"; +- $selector_to_use = ''; +- if ( ':root' === $selector ) { +- $selector_to_use = $class_to_use; +- } else { +- $selector_to_use = "$selector$class_to_use"; +- } +- if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) { +- $stylesheet .= "$selector_to_use {\n\t$property: $value;\n}\n"; +- } else { +- $stylesheet .= $selector_to_use . '{' . "$property:$value;}\n"; +- } +- } +- } +- return $stylesheet; ++ return $theme_settings; + } + + /** + * Takes a tree adhering to the theme.json schema and generates + * the corresponding stylesheet. + * +- * @param array $tree Input tree. ++ * @param WP_Theme_JSON $tree Input tree. ++ * @param string $type Type of stylesheet we want accepts 'all', 'block_styles', and 'css_variables'. + * + * @return string Stylesheet. + */ +-function gutenberg_experimental_global_styles_get_stylesheet( $tree ) { +- $stylesheet = ''; +- $block_data = gutenberg_experimental_global_styles_get_block_data(); +- foreach ( array_keys( $tree ) as $block_name ) { +- if ( +- ! array_key_exists( $block_name, $block_data ) || +- ! array_key_exists( 'selector', $block_data[ $block_name ] ) || +- ! array_key_exists( 'supports', $block_data[ $block_name ] ) +- ) { +- // Skip blocks that haven't declared support, +- // because we don't know to process them. +- continue; +- } ++function gutenberg_experimental_global_styles_get_stylesheet( $tree, $type = 'all' ) { ++ // Check if we can use cached. ++ $can_use_cached = ( ++ ( 'all' === $type ) && ++ ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) && ++ ( ! defined( 'SCRIPT_DEBUG' ) || ! SCRIPT_DEBUG ) && ++ ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) && ++ ! is_admin() ++ ); + +- // Create the CSS Custom Properties for the presets. +- $computed_presets = array(); +- $presets_structure = gutenberg_experimental_global_styles_get_presets_structure(); +- foreach ( $presets_structure as $token => $preset_meta ) { +- $block_preset = gutenberg_experimental_get( $tree[ $block_name ]['settings'], $preset_meta['path'] ); +- if ( ! empty( $block_preset ) ) { +- $computed_presets[ $token ] = array(); +- foreach ( $block_preset as $preset_value ) { +- $computed_presets[ $token ][ $preset_value['slug'] ] = $preset_value[ $preset_meta['key'] ]; +- } +- } ++ if ( $can_use_cached ) { ++ // Check if we have the styles already cached. ++ $cached = get_transient( 'global_styles' ); ++ if ( $cached ) { ++ return $cached; + } +- $token = '--'; +- $preset_prefix = '--wp--preset' . $token; +- $preset_variables = gutenberg_experimental_global_styles_get_css_vars( $computed_presets, $preset_prefix, $token ); +- +- // Create the CSS Custom Properties that are specific to the theme. +- $computed_theme_props = gutenberg_experimental_get( $tree[ $block_name ]['settings'], array( 'custom' ) ); +- $theme_props_prefix = '--wp--custom' . $token; +- $theme_variables = gutenberg_experimental_global_styles_get_css_vars( +- $computed_theme_props, +- $theme_props_prefix, +- $token +- ); +- +- $stylesheet .= gutenberg_experimental_global_styles_resolver_styles( +- $block_data[ $block_name ]['selector'], +- $block_data[ $block_name ]['supports'], +- array_merge( +- gutenberg_experimental_global_styles_flatten_styles_tree( $tree[ $block_name ]['styles'] ), +- $preset_variables, +- $theme_variables +- ) +- ); +- +- $stylesheet .= gutenberg_experimental_global_styles_get_preset_classes( $block_data[ $block_name ]['selector'], $tree[ $block_name ]['settings'] ); + } + +- if ( gutenberg_experimental_global_styles_has_theme_json_support() ) { ++ $stylesheet = $tree->get_stylesheet( $type ); ++ ++ if ( ( 'all' === $type || 'block_styles' === $type ) && gutenberg_experimental_global_styles_has_theme_json_support() ) { + // To support all themes, we added in the block-library stylesheet + // a style rule such as .has-link-color a { color: var(--wp--style--color--link, #00e); } + // so that existing link colors themes used didn't break. +@@ -714,149 +148,31 @@ function gutenberg_experimental_global_styles_get_stylesheet( $tree ) { + $stylesheet .= 'a{color:var(--wp--style--color--link, #00e);}'; + } + +- return $stylesheet; +-} +- +-/** +- * Generates CSS declarations for a block. +- * +- * @param string $block_selector CSS selector for the block. +- * @param array $block_supports A list of properties supported by the block. +- * @param array $block_styles The list of properties/values to be converted to CSS. +- * +- * @return string The corresponding CSS rule. +- */ +-function gutenberg_experimental_global_styles_resolver_styles( $block_selector, $block_supports, $block_styles ) { +- $css_property = ''; +- $css_rule = ''; +- $css_declarations = ''; +- +- foreach ( $block_styles as $property => $value ) { +- // Only convert to CSS: +- // +- // 1) The style attributes the block has declared support for. +- // 2) Any CSS custom property attached to the node. +- if ( +- in_array( $property, $block_supports, true ) || +- strstr( $property, '--' ) +- ) { +- $css_property = gutenberg_experimental_global_styles_get_css_property( $property ); +- +- // Add whitespace if SCRIPT_DEBUG is defined and set to true. +- if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) { +- $css_declarations .= "\t" . $css_property . ': ' . $value . ";\n"; +- } else { +- $css_declarations .= $css_property . ':' . $value . ';'; +- } +- } ++ if ( $can_use_cached ) { ++ // Cache for a minute. ++ // This cache doesn't need to be any longer, we only want to avoid spikes on high-traffic sites. ++ set_transient( 'global_styles', $stylesheet, MINUTE_IN_SECONDS ); + } + +- if ( '' !== $css_declarations ) { +- +- // Add whitespace if SCRIPT_DEBUG is defined and set to true. +- if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) { +- $css_rule .= $block_selector . " {\n"; +- $css_rule .= $css_declarations; +- $css_rule .= "}\n"; +- } else { +- $css_rule .= $block_selector . '{' . $css_declarations . '}'; +- } +- } +- +- return $css_rule; +-} +- +-/** +- * Helper function that merges trees that adhere to the theme.json schema. +- * +- * @param array $core Core origin. +- * @param array $theme Theme origin. +- * @param array $user User origin. An empty array by default. +- * +- * @return array The merged result. +- */ +-function gutenberg_experimental_global_styles_merge_trees( $core, $theme, $user = array() ) { +- $core = gutenberg_experimental_global_styles_normalize_schema( $core ); +- $theme = gutenberg_experimental_global_styles_normalize_schema( $theme ); +- $user = gutenberg_experimental_global_styles_normalize_schema( $user ); +- $result = gutenberg_experimental_global_styles_normalize_schema( array() ); +- +- foreach ( array_keys( $core ) as $block_name ) { +- foreach ( array_keys( $core[ $block_name ]['settings'] ) as $subtree ) { +- $result[ $block_name ]['settings'][ $subtree ] = array_merge( +- $core[ $block_name ]['settings'][ $subtree ], +- $theme[ $block_name ]['settings'][ $subtree ], +- $user[ $block_name ]['settings'][ $subtree ] +- ); +- } +- foreach ( array_keys( $core[ $block_name ]['styles'] ) as $subtree ) { +- $result[ $block_name ]['styles'][ $subtree ] = array_merge( +- $core[ $block_name ]['styles'][ $subtree ], +- $theme[ $block_name ]['styles'][ $subtree ], +- $user[ $block_name ]['styles'][ $subtree ] +- ); +- } +- } +- +- return $result; ++ return $stylesheet; + } + + /** +- * Given a tree, it normalizes it to the expected schema. +- * +- * @param array $tree Source tree to normalize. +- * +- * @return array Normalized tree. ++ * Fetches the preferences for each origin (core, theme, user) ++ * and enqueues the resulting stylesheet. + */ +-function gutenberg_experimental_global_styles_normalize_schema( $tree ) { +- $block_schema = array( +- 'styles' => array( +- 'typography' => array(), +- 'color' => array(), +- ), +- 'settings' => array( +- 'color' => array(), +- 'custom' => array(), +- 'typography' => array(), +- 'spacing' => array(), +- ), +- ); +- +- $normalized_tree = array(); +- $block_data = gutenberg_experimental_global_styles_get_block_data(); +- foreach ( array_keys( $block_data ) as $block_name ) { +- $normalized_tree[ $block_name ] = $block_schema; ++function gutenberg_experimental_global_styles_enqueue_assets() { ++ if ( ! gutenberg_experimental_global_styles_has_theme_json_support() ) { ++ return; + } + +- $tree = array_merge_recursive( +- $normalized_tree, +- $tree +- ); +- +- return $tree; +-} +- +-/** +- * Takes data from the different origins (core, theme, and user) +- * and returns the merged result. +- * +- * @return array Merged trees +- */ +-function gutenberg_experimental_global_styles_get_merged_origins() { +- $core = gutenberg_experimental_global_styles_get_core(); +- $theme = gutenberg_experimental_global_styles_get_theme(); +- $user = gutenberg_experimental_global_styles_get_user(); ++ $settings = gutenberg_get_common_block_editor_settings(); ++ $theme_support_data = gutenberg_experimental_global_styles_get_theme_support_settings( $settings ); + +- return gutenberg_experimental_global_styles_merge_trees( $core, $theme, $user ); +-} ++ $resolver = new WP_Theme_JSON_Resolver(); ++ $all = $resolver->get_origin( $theme_support_data ); + +-/** +- * Fetches the preferences for each origin (core, theme, user) +- * and enqueues the resulting stylesheet. +- */ +-function gutenberg_experimental_global_styles_enqueue_assets() { +- $merged = gutenberg_experimental_global_styles_get_merged_origins(); +- $stylesheet = gutenberg_experimental_global_styles_get_stylesheet( $merged ); ++ $stylesheet = gutenberg_experimental_global_styles_get_stylesheet( $all ); + if ( empty( $stylesheet ) ) { + return; + } +@@ -866,28 +182,6 @@ function gutenberg_experimental_global_styles_enqueue_assets() { + wp_enqueue_style( 'global-styles' ); + } + +-/** +- * Returns the default config for editor features, +- * or an empty array if none found. +- * +- * @param array $config Config to extract values from. +- * @return array Default features config for the editor. +- */ +-function gutenberg_experimental_global_styles_get_editor_settings( $config ) { +- $settings = array(); +- foreach ( array_keys( $config ) as $context ) { +- if ( +- empty( $config[ $context ]['settings'] ) || +- ! is_array( $config[ $context ]['settings'] ) +- ) { +- $settings[ $context ] = array(); +- } else { +- $settings[ $context ] = $config[ $context ]['settings']; +- } +- } +- return $settings; +-} +- + /** + * Adds the necessary data for the Global Styles client UI to the block settings. + * +@@ -895,85 +189,194 @@ function gutenberg_experimental_global_styles_get_editor_settings( $config ) { + * @return array New block editor settings + */ + function gutenberg_experimental_global_styles_settings( $settings ) { +- $merged = gutenberg_experimental_global_styles_get_merged_origins(); +- +- // STEP 1: ADD FEATURES +- // These need to be added to settings always. +- // We also need to unset the deprecated settings defined by core. +- $settings['__experimentalFeatures'] = gutenberg_experimental_global_styles_get_editor_settings( $merged ); +- ++ $theme_support_data = gutenberg_experimental_global_styles_get_theme_support_settings( $settings ); + unset( $settings['colors'] ); +- unset( $settings['gradients'] ); +- unset( $settings['fontSizes'] ); + unset( $settings['disableCustomColors'] ); +- unset( $settings['disableCustomGradients'] ); + unset( $settings['disableCustomFontSizes'] ); ++ unset( $settings['disableCustomGradients'] ); + unset( $settings['enableCustomLineHeight'] ); + unset( $settings['enableCustomUnits'] ); ++ unset( $settings['fontSizes'] ); ++ unset( $settings['gradients'] ); ++ ++ $resolver = new WP_Theme_JSON_Resolver(); ++ $origin = 'theme'; ++ if ( ++ gutenberg_experimental_global_styles_has_theme_json_support() && ++ gutenberg_is_fse_theme() ++ ) { ++ // Only lookup for the user data if we need it. ++ $origin = 'user'; ++ } ++ $tree = $resolver->get_origin( $theme_support_data, $origin ); ++ ++ // STEP 1: ADD FEATURES ++ // ++ // These need to be always added to the editor settings, ++ // even for themes that don't support theme.json. ++ // An example of this is that the presets are configured ++ // from the theme support data. ++ $settings['__experimentalFeatures'] = $tree->get_settings(); + + // STEP 2 - IF EDIT-SITE, ADD DATA REQUIRED FOR GLOBAL STYLES SIDEBAR +- // The client needs some information to be able to access/update the user styles. +- // We only do this if the theme has support for theme.json, though, +- // as an indicator that the theme will know how to combine this with its stylesheet. ++ // ++ // In the site editor, the user can change styles, so the client ++ // needs the ability to create them. Hence, we pass it some data ++ // for this: base styles (core+theme) and the ID of the user CPT. + $screen = get_current_screen(); + if ( + ! empty( $screen ) && + function_exists( 'gutenberg_is_edit_site_page' ) && + gutenberg_is_edit_site_page( $screen->id ) && +- gutenberg_experimental_global_styles_has_theme_json_support() ++ gutenberg_experimental_global_styles_has_theme_json_support() && ++ gutenberg_is_fse_theme() + ) { +- $settings['__experimentalGlobalStylesUserEntityId'] = gutenberg_experimental_global_styles_get_user_cpt_id(); +- $settings['__experimentalGlobalStylesContexts'] = gutenberg_experimental_global_styles_get_block_data(); +- $settings['__experimentalGlobalStylesBaseStyles'] = gutenberg_experimental_global_styles_merge_trees( +- gutenberg_experimental_global_styles_get_core(), +- gutenberg_experimental_global_styles_get_theme() +- ); +- } else { +- // STEP 3 - OTHERWISE, ADD STYLES ++ $user_cpt_id = WP_Theme_JSON_Resolver::get_user_custom_post_type_id(); ++ $base_styles = $resolver->get_origin( $theme_support_data, 'theme' )->get_raw_data(); ++ ++ $settings['__experimentalGlobalStylesUserEntityId'] = $user_cpt_id; ++ $settings['__experimentalGlobalStylesBaseStyles'] = $base_styles; ++ } elseif ( gutenberg_experimental_global_styles_has_theme_json_support() ) { ++ // STEP 3 - ADD STYLES IF THEME HAS SUPPORT + // + // If we are in a block editor context, but not in edit-site, +- // we need to add the styles via the settings. This is because +- // we want them processed as if they were added via add_editor_styles, +- // which adds the editor wrapper class. +- $settings['styles'][] = array( 'css' => gutenberg_experimental_global_styles_get_stylesheet( $merged ) ); ++ // we add the styles via the settings, so the editor knows that ++ // some of these should be added the wrapper class, ++ // as if they were added via add_editor_styles. ++ $settings['styles'][] = array( ++ 'css' => gutenberg_experimental_global_styles_get_stylesheet( $tree, 'css_variables' ), ++ '__experimentalNoWrapper' => true, ++ ); ++ $settings['styles'][] = array( ++ 'css' => gutenberg_experimental_global_styles_get_stylesheet( $tree, 'block_styles' ), ++ ); + } + + return $settings; + } + + /** +- * Registers a Custom Post Type to store the user's origin config. ++ * Register CPT to store/access user data. ++ * ++ * @return array|undefined + */ +-function gutenberg_experimental_global_styles_register_cpt() { ++function gutenberg_experimental_global_styles_register_user_cpt() { + if ( ! gutenberg_experimental_global_styles_has_theme_json_support() ) { + return; + } + +- $args = array( +- 'label' => __( 'Global Styles', 'gutenberg' ), +- 'description' => 'CPT to store user design tokens', +- 'public' => false, +- 'show_ui' => false, +- 'show_in_rest' => true, +- 'rest_base' => '__experimental/global-styles', +- 'capabilities' => array( +- 'read' => 'edit_theme_options', +- 'create_posts' => 'edit_theme_options', +- 'edit_posts' => 'edit_theme_options', +- 'edit_published_posts' => 'edit_theme_options', +- 'delete_published_posts' => 'edit_theme_options', +- 'edit_others_posts' => 'edit_theme_options', +- 'delete_others_posts' => 'edit_theme_options', +- ), +- 'map_meta_cap' => true, +- 'supports' => array( +- 'editor', +- 'revisions', +- ), +- ); +- register_post_type( 'wp_global_styles', $args ); ++ WP_Theme_JSON_Resolver::register_user_custom_post_type(); + } + +-add_action( 'init', 'gutenberg_experimental_global_styles_register_cpt' ); +-add_filter( 'block_editor_settings', 'gutenberg_experimental_global_styles_settings' ); ++add_action( 'init', 'gutenberg_experimental_global_styles_register_user_cpt' ); ++add_filter( 'block_editor_settings', 'gutenberg_experimental_global_styles_settings', PHP_INT_MAX ); + add_action( 'wp_enqueue_scripts', 'gutenberg_experimental_global_styles_enqueue_assets' ); ++ ++ ++/** ++ * Sanitizes global styles user content removing unsafe rules. ++ * ++ * @param string $content Post content to filter. ++ * @return string Filtered post content with unsafe rules removed. ++ */ ++function gutenberg_global_styles_filter_post( $content ) { ++ $decoded_data = json_decode( stripslashes( $content ), true ); ++ $json_decoding_error = json_last_error(); ++ if ( ++ JSON_ERROR_NONE === $json_decoding_error && ++ is_array( $decoded_data ) && ++ isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) && ++ $decoded_data['isGlobalStylesUserThemeJSON'] ++ ) { ++ unset( $decoded_data['isGlobalStylesUserThemeJSON'] ); ++ $theme_json = new WP_Theme_JSON( $decoded_data ); ++ $theme_json->remove_insecure_properties(); ++ $data_to_encode = $theme_json->get_raw_data(); ++ $data_to_encode['isGlobalStylesUserThemeJSON'] = true; ++ return wp_json_encode( $data_to_encode ); ++ } ++ return $content; ++} ++ ++/** ++ * Adds the filters to filter global styles user theme.json. ++ */ ++function gutenberg_global_styles_kses_init_filters() { ++ add_filter( 'content_save_pre', 'gutenberg_global_styles_filter_post' ); ++} ++ ++/** ++ * Removes the filters to filter global styles user theme.json. ++ */ ++function gutenberg_global_styles_kses_remove_filters() { ++ remove_filter( 'content_save_pre', 'gutenberg_global_styles_filter_post' ); ++} ++ ++/** ++ * Register global styles kses filters if the user does not have unfiltered_html capability. ++ * ++ * @uses render_block_core_navigation() ++ * @throws WP_Error An WP_Error exception parsing the block definition. ++ */ ++function gutenberg_global_styles_kses_init() { ++ gutenberg_global_styles_kses_remove_filters(); ++ if ( ! current_user_can( 'unfiltered_html' ) ) { ++ gutenberg_global_styles_kses_init_filters(); ++ } ++} ++ ++/** ++ * This filter is the last being executed on force_filtered_html_on_import. ++ * If the input of the filter is true it means we are in an import situation and should ++ * enable kses, independently of the user capabilities. ++ * So in that case we call gutenberg_global_styles_kses_init_filters; ++ * ++ * @param string $arg Input argument of the filter. ++ * @return string Exactly what was passed as argument. ++ */ ++function gutenberg_global_styles_force_filtered_html_on_import_filter( $arg ) { ++ // force_filtered_html_on_import is true we need to init the global styles kses filters. ++ if ( $arg ) { ++ gutenberg_global_styles_kses_init_filters(); ++ } ++ return $arg; ++} ++ ++/** ++ * This filter is the last being executed on force_filtered_html_on_import. ++ * If the input of the filter is true it means we are in an import situation and should ++ * enable kses, independently of the user capabilities. ++ * So in that case we call gutenberg_global_styles_kses_init_filters; ++ * ++ * @param bool $allow_css Whether the CSS in the test string is considered safe. ++ * @param bool $css_test_string The CSS string to test.. ++ * @return bool If $allow_css is true it returns true. ++ * If $allow_css is false and the CSS rule is referencing a WordPress css variable it returns true. ++ * Otherwise the function return false. ++ */ ++function gutenberg_global_styles_include_support_for_wp_variables( $allow_css, $css_test_string ) { ++ if ( $allow_css ) { ++ return $allow_css; ++ } ++ $allowed_preset_attributes = array( ++ 'background', ++ 'background-color', ++ 'color', ++ 'font-family', ++ 'font-size', ++ ); ++ $parts = explode( ':', $css_test_string, 2 ); ++ ++ if ( ! in_array( trim( $parts[0] ), $allowed_preset_attributes, true ) ) { ++ return $allow_css; ++ } ++ return ! ! preg_match( '/^var\(--wp-[a-zA-Z0-9\-]+\)$/', trim( $parts[1] ) ); ++} ++ ++ ++add_action( 'init', 'gutenberg_global_styles_kses_init' ); ++add_action( 'set_current_user', 'gutenberg_global_styles_kses_init' ); ++add_filter( 'force_filtered_html_on_import', 'gutenberg_global_styles_force_filtered_html_on_import_filter', 999 ); ++add_filter( 'safecss_filter_attr_allow_css', 'gutenberg_global_styles_include_support_for_wp_variables', 10, 2 ); ++// This filter needs to be executed last. ++ +Skip. Plugin only. +diff --git a/lib/load.php b/lib/load.php +index 42ebfe7bb5..cff4bb2f49 100644 +--- a/lib/load.php ++++ b/lib/load.php +@@ -9,6 +9,8 @@ if ( ! defined( 'ABSPATH' ) ) { + die( 'Silence is golden.' ); + } + ++require_once __DIR__ . '/upgrade.php'; ++ + /** + * Checks whether the Gutenberg experiment is enabled. + * +@@ -30,102 +32,86 @@ if ( class_exists( 'WP_REST_Controller' ) ) { + * Start: Include for phase 2 + */ + if ( ! class_exists( 'WP_REST_Sidebars_Controller' ) ) { +- require_once dirname( __FILE__ ) . '/class-wp-rest-sidebars-controller.php'; ++ require_once __DIR__ . '/class-wp-rest-sidebars-controller.php'; + } + if ( ! class_exists( 'WP_REST_Widget_Types_Controller' ) ) { +- require_once dirname( __FILE__ ) . '/class-wp-rest-widget-types-controller.php'; ++ require_once __DIR__ . '/class-wp-rest-widget-types-controller.php'; + } + if ( ! class_exists( 'WP_REST_Widgets_Controller' ) ) { +- require_once dirname( __FILE__ ) . '/class-wp-rest-widgets-controller.php'; +- } +- if ( ! class_exists( 'WP_REST_Block_Directory_Controller' ) ) { +- require dirname( __FILE__ ) . '/class-wp-rest-block-directory-controller.php'; +- } +- if ( ! class_exists( 'WP_REST_Block_Types_Controller' ) ) { +- require dirname( __FILE__ ) . '/class-wp-rest-block-types-controller.php'; ++ require_once __DIR__ . '/class-wp-rest-widgets-controller.php'; + } + if ( ! class_exists( 'WP_REST_Menus_Controller' ) ) { +- require_once dirname( __FILE__ ) . '/class-wp-rest-menus-controller.php'; ++ require_once __DIR__ . '/class-wp-rest-menus-controller.php'; + } + if ( ! class_exists( 'WP_REST_Menu_Items_Controller' ) ) { +- require_once dirname( __FILE__ ) . '/class-wp-rest-menu-items-controller.php'; ++ require_once __DIR__ . '/class-wp-rest-menu-items-controller.php'; + } + if ( ! class_exists( 'WP_REST_Menu_Locations_Controller' ) ) { +- require_once dirname( __FILE__ ) . '/class-wp-rest-menu-locations-controller.php'; ++ require_once __DIR__ . '/class-wp-rest-menu-locations-controller.php'; + } + if ( ! class_exists( 'WP_Rest_Customizer_Nonces' ) ) { +- require_once dirname( __FILE__ ) . '/class-wp-rest-customizer-nonces.php'; +- } +- if ( ! class_exists( 'WP_REST_Image_Editor_Controller' ) ) { +- require dirname( __FILE__ ) . '/class-wp-rest-image-editor-controller.php'; +- } +- if ( ! class_exists( 'WP_REST_Plugins_Controller' ) ) { +- require_once dirname( __FILE__ ) . '/class-wp-rest-plugins-controller.php'; ++ require_once __DIR__ . '/class-wp-rest-customizer-nonces.php'; + } + if ( ! class_exists( 'WP_REST_Post_Format_Search_Handler' ) ) { +- require_once dirname( __FILE__ ) . '/class-wp-rest-post-format-search-handler.php'; ++ require_once __DIR__ . '/class-wp-rest-post-format-search-handler.php'; + } + if ( ! class_exists( 'WP_REST_Term_Search_Handler' ) ) { +- require_once dirname( __FILE__ ) . '/class-wp-rest-term-search-handler.php'; ++ require_once __DIR__ . '/class-wp-rest-term-search-handler.php'; + } + if ( ! class_exists( 'WP_REST_Batch_Controller' ) ) { +- require_once dirname( __FILE__ ) . '/class-wp-rest-batch-controller.php'; ++ require_once __DIR__ . '/class-wp-rest-batch-controller.php'; ++ } ++ if ( ! class_exists( 'WP_REST_Templates_Controller' ) ) { ++ require_once __DIR__ . '/full-site-editing/class-wp-rest-templates-controller.php'; + } + /** + * End: Include for phase 2 + */ + +- require dirname( __FILE__ ) . '/rest-api.php'; +-} +- +-if ( ! class_exists( 'WP_Block_Patterns_Registry' ) ) { +- require dirname( __FILE__ ) . '/class-wp-block-patterns-registry.php'; +-} +- +-if ( ! class_exists( 'WP_Block_Pattern_Categories_Registry' ) ) { +- require dirname( __FILE__ ) . '/class-wp-block-pattern-categories-registry.php'; ++ require __DIR__ . '/rest-api.php'; + } + +-if ( ! class_exists( 'WP_Block' ) ) { +- require dirname( __FILE__ ) . '/class-wp-block.php'; +-} +- +-if ( ! class_exists( 'WP_Block_List' ) ) { +- require dirname( __FILE__ ) . '/class-wp-block-list.php'; +-} + if ( ! class_exists( 'WP_Widget_Block' ) ) { +- require_once dirname( __FILE__ ) . '/class-wp-widget-block.php'; ++ require_once __DIR__ . '/class-wp-widget-block.php'; + } +-require_once dirname( __FILE__ ) . '/widgets-page.php'; + +-require dirname( __FILE__ ) . '/compat.php'; +-require dirname( __FILE__ ) . '/utils.php'; ++require_once __DIR__ . '/widgets-page.php'; ++ ++require __DIR__ . '/compat.php'; ++require __DIR__ . '/utils.php'; ++require __DIR__ . '/editor-settings.php'; + +-// Include FSE related files only if the experiment is enabled. +-if ( gutenberg_is_experiment_enabled( 'gutenberg-full-site-editing' ) ) { +- require dirname( __FILE__ ) . '/templates.php'; +- require dirname( __FILE__ ) . '/template-parts.php'; +- require dirname( __FILE__ ) . '/template-loader.php'; +- require dirname( __FILE__ ) . '/edit-site-page.php'; +- require dirname( __FILE__ ) . '/edit-site-export.php'; ++if ( ! class_exists( 'WP_Block_Template ' ) ) { ++ require __DIR__ . '/full-site-editing/class-wp-block-template.php'; + } ++require __DIR__ . '/full-site-editing/full-site-editing.php'; ++require __DIR__ . '/full-site-editing/block-templates.php'; ++require __DIR__ . '/full-site-editing/default-template-types.php'; ++require __DIR__ . '/full-site-editing/templates-utils.php'; ++require __DIR__ . '/full-site-editing/page-templates.php'; ++require __DIR__ . '/full-site-editing/templates.php'; ++require __DIR__ . '/full-site-editing/template-parts.php'; ++require __DIR__ . '/full-site-editing/template-loader.php'; ++require __DIR__ . '/full-site-editing/edit-site-page.php'; ++require __DIR__ . '/full-site-editing/edit-site-export.php'; + +-require dirname( __FILE__ ) . '/block-patterns.php'; +-require dirname( __FILE__ ) . '/blocks.php'; +-require dirname( __FILE__ ) . '/client-assets.php'; +-require dirname( __FILE__ ) . '/block-directory.php'; +-require dirname( __FILE__ ) . '/demo.php'; +-require dirname( __FILE__ ) . '/widgets.php'; +-require dirname( __FILE__ ) . '/navigation.php'; +-require dirname( __FILE__ ) . '/navigation-page.php'; +-require dirname( __FILE__ ) . '/experiments-page.php'; +-require dirname( __FILE__ ) . '/global-styles.php'; ++require __DIR__ . '/blocks.php'; ++require __DIR__ . '/client-assets.php'; ++require __DIR__ . '/demo.php'; ++require __DIR__ . '/widgets.php'; ++require __DIR__ . '/navigation.php'; ++require __DIR__ . '/navigation-page.php'; ++require __DIR__ . '/experiments-page.php'; ++require __DIR__ . '/class-wp-theme-json.php'; ++require __DIR__ . '/class-wp-theme-json-resolver.php'; ++require __DIR__ . '/global-styles.php'; + + if ( ! class_exists( 'WP_Block_Supports' ) ) { +- require_once dirname( __FILE__ ) . '/class-wp-block-supports.php'; ++ require_once __DIR__ . '/class-wp-block-supports.php'; + } +-require dirname( __FILE__ ) . '/block-supports/generated-classname.php'; +-require dirname( __FILE__ ) . '/block-supports/colors.php'; +-require dirname( __FILE__ ) . '/block-supports/align.php'; +-require dirname( __FILE__ ) . '/block-supports/typography.php'; +-require dirname( __FILE__ ) . '/block-supports/custom-classname.php'; ++require __DIR__ . '/block-supports/generated-classname.php'; ++require __DIR__ . '/block-supports/colors.php'; ++require __DIR__ . '/block-supports/align.php'; ++require __DIR__ . '/block-supports/typography.php'; ++require __DIR__ . '/block-supports/custom-classname.php'; ++require __DIR__ . '/block-supports/border.php'; +diff --git a/lib/navigation-page.php b/lib/navigation-page.php +index 16be155850..98398cc238 100644 +--- a/lib/navigation-page.php ++++ b/lib/navigation-page.php +@@ -32,44 +32,12 @@ function gutenberg_navigation_init( $hook ) { + return; + } + +- // Media settings. +- $max_upload_size = wp_max_upload_size(); +- if ( ! $max_upload_size ) { +- $max_upload_size = 0; +- } +- +- /** This filter is documented in wp-admin/includes/media.php */ +- $image_size_names = apply_filters( +- 'image_size_names_choose', ++ $settings = array_merge( ++ gutenberg_get_common_block_editor_settings(), + array( +- 'thumbnail' => __( 'Thumbnail', 'gutenberg' ), +- 'medium' => __( 'Medium', 'gutenberg' ), +- 'large' => __( 'Large', 'gutenberg' ), +- 'full' => __( 'Full Size', 'gutenberg' ), ++ 'blockNavMenus' => get_theme_support( 'block-nav-menus' ), + ) + ); +- +- $available_image_sizes = array(); +- foreach ( $image_size_names as $image_size_slug => $image_size_name ) { +- $available_image_sizes[] = array( +- 'slug' => $image_size_slug, +- 'name' => $image_size_name, +- ); +- } +- +- $settings = array( +- 'imageSizes' => $available_image_sizes, +- 'isRTL' => is_rtl(), +- 'maxUploadFileSize' => $max_upload_size, +- 'blockNavMenus' => get_theme_support( 'block-nav-menus' ), +- ); +- +- list( $font_sizes, ) = (array) get_theme_support( 'editor-font-sizes' ); +- +- if ( false !== $font_sizes ) { +- $settings['fontSizes'] = $font_sizes; +- } +- + $settings = gutenberg_experimental_global_styles_settings( $settings ); + + wp_add_inline_script( +@@ -78,7 +46,7 @@ function gutenberg_navigation_init( $hook ) { + 'wp.domReady( function() { + wp.editNavigation.initialize( "navigation-editor", %s ); + } );', +- wp_json_encode( gutenberg_experiments_editor_settings( $settings ) ) ++ wp_json_encode( $settings ) + ) + ); + +diff --git a/lib/patterns/heading-paragraph.php b/lib/patterns/heading-paragraph.php +deleted file mode 100644 +index 9a8b160a78..0000000000 +--- a/lib/patterns/heading-paragraph.php ++++ /dev/null +@@ -1,14 +0,0 @@ +- __( 'Heading and paragraph', 'gutenberg' ), +- 'content' => "\n
\n

2.
" . __( 'Which treats of the first sally the ingenious Don Quixote made from home', 'gutenberg' ) . "

\n\n\n\n

" . __( 'These preliminaries settled, he did not care to put off any longer the execution of his design, urged on to it by the thought of all the world was losing by his delay, seeing what wrongs he intended to right, grievances to redress, injustices to repair, abuses to remove, and duties to discharge.', 'gutenberg' ) . "

\n
\n", +- 'viewportWidth' => 1000, +- 'categories' => array( 'text' ), +- 'description' => _x( 'A heading preceded by a chapter number, and followed by a paragraph.', 'Block pattern description', 'gutenberg' ), +-); +diff --git a/lib/patterns/large-header-button.php b/lib/patterns/large-header-button.php +deleted file mode 100644 +index 1d71e5703f..0000000000 +--- a/lib/patterns/large-header-button.php ++++ /dev/null +@@ -1,14 +0,0 @@ +- __( 'Large header with a heading and a button ', 'gutenberg' ), +- 'content' => "\n
\n
\n
\n
\n
\n\n\n\n
\n
\n\n\n\n

" . __( 'Thou hast seen', 'gutenberg' ) . '
' . __( 'nothing yet', 'gutenberg' ) . "

\n\n\n\n\n\n\n\n
\n
\n\n\n\n
\n
\n
\n
\n
\n", +- 'viewportWidth' => 1000, +- 'categories' => array( 'header' ), +- 'description' => _x( 'A large hero section with a bright gradient background, a big heading and a filled button.', 'Block pattern description', 'gutenberg' ), +-); +diff --git a/lib/patterns/large-header.php b/lib/patterns/large-header.php +deleted file mode 100644 +index 0213bf8bfe..0000000000 +--- a/lib/patterns/large-header.php ++++ /dev/null +@@ -1,14 +0,0 @@ +- __( 'Large header with a heading', 'gutenberg' ), +- 'content' => "\n
\n

" . __( 'Don Quixote', 'gutenberg' ) . "

\n
\n", +- 'viewportWidth' => 1000, +- 'categories' => array( 'header' ), +- 'description' => _x( 'A large hero section with an example background image and a heading in the center.', 'Block pattern description', 'gutenberg' ), +-); +diff --git a/lib/patterns/quote.php b/lib/patterns/quote.php +deleted file mode 100644 +index 0e1e0f3b24..0000000000 +--- a/lib/patterns/quote.php ++++ /dev/null +@@ -1,14 +0,0 @@ +- __( 'Quote', 'gutenberg' ), +- 'content' => "\n
\n
\""
\n\n\n\n

" . __( '"Do you see over yonder, friend Sancho, thirty or forty hulking giants? I intend to do battle with them and slay them."', 'gutenberg' ) . '

' . __( '— Don Quixote', 'gutenberg' ) . "
\n\n\n\n
\n
\n", +- 'viewportWidth' => 800, +- 'categories' => array( 'text' ), +- 'description' => _x( 'A quote and citation with an image above, and a separator at the bottom.', 'Block pattern description', 'gutenberg' ), +-); +diff --git a/lib/patterns/text-three-columns-buttons.php b/lib/patterns/text-three-columns-buttons.php +deleted file mode 100644 +index 629c839b89..0000000000 +--- a/lib/patterns/text-three-columns-buttons.php ++++ /dev/null +@@ -1,14 +0,0 @@ +- __( 'Three columns of text with buttons', 'gutenberg' ), +- 'categories' => array( 'columns' ), +- 'content' => "\n
\n
\n
\n

" . __( 'Which treats of the character and pursuits of the famous Don Quixote of La Mancha.', 'gutenberg' ) . "

\n\n\n\n\n
\n\n\n\n
\n

" . __( 'Which treats of the first sally the ingenious Don Quixote made from home.', 'gutenberg' ) . "

\n\n\n\n\n
\n\n\n\n
\n

" . __( 'Wherein is related the droll way in which Don Quixote had himself dubbed a knight.', 'gutenberg' ) . "

\n\n\n\n\n
\n
\n
\n", +- 'viewportWidth' => 1000, +- 'description' => _x( 'Three small columns of text, each with an outlined button with rounded corners at the bottom.', 'Block pattern description', 'gutenberg' ), +-); +diff --git a/lib/patterns/text-two-columns-with-images.php b/lib/patterns/text-two-columns-with-images.php +deleted file mode 100644 +index c4978acbc3..0000000000 +--- a/lib/patterns/text-two-columns-with-images.php ++++ /dev/null +@@ -1,13 +0,0 @@ +- __( 'Two columns of text with images', 'gutenberg' ), +- 'categories' => array( 'columns' ), +- 'content' => "\n
\n
\n
\n
\"\"/
\n\n\n\n

" . __( 'They must know, then, that the above-named gentleman whenever he was at leisure (which was mostly all the year round) gave himself up to reading books of chivalry with such ardour and avidity that he almost entirely neglected the pursuit of his field-sports, and even the management of his property; and to such a pitch did his eagerness and infatuation go that he sold many an acre of tillageland to buy books of chivalry to read, and brought home as many of them as he could get.', 'gutenberg' ) . "

\n
\n\n\n\n
\n
\"\"/
\n\n\n\n

" . __( 'But of all there were none he liked so well as those of the famous Feliciano de Silva\'s composition, for their lucidity of style and complicated conceits were as pearls in his sight, particularly when in his reading he came upon courtships and cartels, where he often found passages like "the reason of the unreason with which my reason is afflicted so weakens my reason that with reason I murmur at your beauty;" or again, "the high heavens render you deserving of the desert your greatness deserves."', 'gutenberg' ) . "

\n
\n
\n
\n", +- 'description' => _x( 'Two columns of text, each with an image on top.', 'Block pattern description', 'gutenberg' ), +-); +diff --git a/lib/patterns/text-two-columns.php b/lib/patterns/text-two-columns.php +deleted file mode 100644 +index ca40c74289..0000000000 +--- a/lib/patterns/text-two-columns.php ++++ /dev/null +@@ -1,13 +0,0 @@ +- __( 'Two columns of text', 'gutenberg' ), +- 'categories' => array( 'columns' ), +- 'content' => "\n
\n

" . __( 'Which treats of the character and pursuits of the famous gentleman Don Quixote of La Mancha', 'gutenberg' ) . "

\n\n\n\n
\n
\n

" . __( 'In a village of La Mancha, the name of which I have no desire to call to mind, there lived not long since one of those gentlemen that keep a lance in the lance-rack, an old buckler, a lean hack, and a greyhound for coursing. An olla of rather more beef than mutton, a salad on most nights, scraps on Saturdays, lentils on Fridays, and a pigeon or so extra on Sundays, made away with three-quarters of his income.', 'gutenberg' ) . "

\n
\n\n\n\n
\n

" . __( 'The rest of it went in a doublet of fine cloth and velvet breeches and shoes to match for holidays, while on week-days he made a brave figure in his best homespun. He had in his house a housekeeper past forty, a niece under twenty, and a lad for the field and market-place, who used to saddle the hack as well as handle the bill-hook. The age of this gentleman of ours was bordering on fifty; he was of a hardy habit, spare, gaunt-featured, a very early riser and a great sportsman.', 'gutenberg' ) . "

\n
\n
\n
\n", +- 'description' => _x( 'Two columns of text preceded by a long heading.', 'Block pattern description', 'gutenberg' ), +-); +diff --git a/lib/patterns/three-buttons.php b/lib/patterns/three-buttons.php +deleted file mode 100644 +index 2f60d3cf00..0000000000 +--- a/lib/patterns/three-buttons.php ++++ /dev/null +@@ -1,14 +0,0 @@ +- __( 'Three buttons', 'gutenberg' ), +- 'content' => "\n\n", +- 'viewportWidth' => 600, +- 'categories' => array( 'buttons' ), +- 'description' => _x( 'Three filled buttons with rounded corners, side by side.', 'Block pattern description', 'gutenberg' ), +-); +diff --git a/lib/patterns/two-buttons.php b/lib/patterns/two-buttons.php +deleted file mode 100644 +index 90564c4c30..0000000000 +--- a/lib/patterns/two-buttons.php ++++ /dev/null +@@ -1,14 +0,0 @@ +- __( 'Two buttons', 'gutenberg' ), +- 'content' => "\n\n", +- 'viewportWidth' => 500, +- 'categories' => array( 'buttons' ), +- 'description' => _x( 'Two buttons, one filled and one outlined, side by side.', 'Block pattern description', 'gutenberg' ), +-); +diff --git a/lib/patterns/two-images.php b/lib/patterns/two-images.php +deleted file mode 100644 +index 6b71853f3f..0000000000 +--- a/lib/patterns/two-images.php ++++ /dev/null +@@ -1,13 +0,0 @@ +- __( 'Two images side by side', 'gutenberg' ), +- 'categories' => array( 'gallery' ), +- 'description' => _x( 'An image gallery with two example images.', 'Block pattern description', 'gutenberg' ), +- 'content' => "\n\n", +-); +diff --git a/lib/rest-api.php b/lib/rest-api.php +index 8ad8653091..add3d35f24 100644 +--- a/lib/rest-api.php ++++ b/lib/rest-api.php +@@ -10,142 +10,6 @@ if ( ! defined( 'ABSPATH' ) ) { + die( 'Silence is golden.' ); + } + +-/** +- * Handle a failing oEmbed proxy request to try embedding as a shortcode. +- * +- * @see https://core.trac.wordpress.org/ticket/45447 +- * +- * @since 2.3.0 +- * +- * @param WP_HTTP_Response|WP_Error $response The REST Request response. +- * @param WP_REST_Server $handler ResponseHandler instance (usually WP_REST_Server). +- * @param WP_REST_Request $request Request used to generate the response. +- * @return WP_HTTP_Response|object|WP_Error The REST Request response. +- */ +-function gutenberg_filter_oembed_result( $response, $handler, $request ) { +- if ( ! is_wp_error( $response ) || 'oembed_invalid_url' !== $response->get_error_code() || +- '/oembed/1.0/proxy' !== $request->get_route() ) { +- return $response; +- } +- +- // Try using a classic embed instead. +- global $wp_embed; +- $html = $wp_embed->shortcode( array(), $_GET['url'] ); +- if ( ! $html ) { +- return $response; +- } +- +- global $wp_scripts; +- +- // Check if any scripts were enqueued by the shortcode, and include them in +- // the response. +- $enqueued_scripts = array(); +- foreach ( $wp_scripts->queue as $script ) { +- $enqueued_scripts[] = $wp_scripts->registered[ $script ]->src; +- } +- +- return array( +- 'provider_name' => __( 'Embed Handler', 'gutenberg' ), +- 'html' => $html, +- 'scripts' => $enqueued_scripts, +- ); +-} +-add_filter( 'rest_request_after_callbacks', 'gutenberg_filter_oembed_result', 10, 3 ); +- +-/** +- * Add fields required for site editing to the /themes endpoint. +- * +- * @todo Remove once Gutenberg's minimum required WordPress version is v5.5. +- * @see https://core.trac.wordpress.org/ticket/49906 +- * @see https://core.trac.wordpress.org/changeset/47921 +- * +- * @param WP_REST_Response $response The response object. +- * @param WP_Theme $theme Theme object used to create response. +- * @param WP_REST_Request $request Request object. +- */ +-function gutenberg_filter_rest_prepare_theme( $response, $theme, $request ) { +- $data = $response->get_data(); +- $fields = array_keys( $data ); +- +- /** +- * The following is basically copied from Core's WP_REST_Themes_Controller::prepare_item_for_response() +- * (as of WP v5.5), with `rest_is_field_included()` replaced by `! in_array()`. +- * This makes sure that we add all the fields that are missing from Core. +- * +- * @see https://github.com/WordPress/WordPress/blob/019bc2d244c4d536338d2c634419583e928143df/wp-includes/rest-api/endpoints/class-wp-rest-themes-controller.php#L118-L167 +- */ +- if ( ! in_array( 'stylesheet', $fields, true ) ) { +- $data['stylesheet'] = $theme->get_stylesheet(); +- } +- +- if ( ! in_array( 'template', $fields, true ) ) { +- /** +- * Use the get_template() method, not the 'Template' header, for finding the template. +- * The 'Template' header is only good for what was written in the style.css, while +- * get_template() takes into account where WordPress actually located the theme and +- * whether it is actually valid. +- */ +- $data['template'] = $theme->get_template(); +- } +- +- $plain_field_mappings = array( +- 'requires_php' => 'RequiresPHP', +- 'requires_wp' => 'RequiresWP', +- 'textdomain' => 'TextDomain', +- 'version' => 'Version', +- ); +- +- foreach ( $plain_field_mappings as $field => $header ) { +- if ( ! in_array( $field, $fields, true ) ) { +- $data[ $field ] = $theme->get( $header ); +- } +- } +- +- if ( ! in_array( 'screenshot', $fields, true ) ) { +- // Using $theme->get_screenshot() with no args to get absolute URL. +- $data['screenshot'] = $theme->get_screenshot() ? $theme->get_screenshot() : ''; +- } +- +- $rich_field_mappings = array( +- 'author' => 'Author', +- 'author_uri' => 'AuthorURI', +- 'description' => 'Description', +- 'name' => 'Name', +- 'tags' => 'Tags', +- 'theme_uri' => 'ThemeURI', +- ); +- +- foreach ( $rich_field_mappings as $field => $header ) { +- if ( ! in_array( $field, $fields, true ) ) { +- $data[ $field ]['raw'] = $theme->display( $header, false, true ); +- $data[ $field ]['rendered'] = $theme->display( $header ); +- } +- } +- +- $response->set_data( $data ); +- return $response; +-} +-add_filter( 'rest_prepare_theme', 'gutenberg_filter_rest_prepare_theme', 10, 3 ); +- +-/** +- * Registers the block directory. +- * +- * @since 6.5.0 +- */ +-function gutenberg_register_rest_block_directory() { +- $block_directory_controller = new WP_REST_Block_Directory_Controller(); +- $block_directory_controller->register_routes(); +-} +-add_filter( 'rest_api_init', 'gutenberg_register_rest_block_directory' ); +- +-/** +- * Registers the Block types REST API routes. +- */ +-function gutenberg_register_block_type() { +- $block_types = new WP_REST_Block_Types_Controller(); +- $block_types->register_routes(); +-} +-add_action( 'rest_api_init', 'gutenberg_register_block_type' ); + /** + * Registers the menu locations area REST API routes. + */ +@@ -164,16 +28,6 @@ function gutenberg_register_rest_customizer_nonces() { + } + add_action( 'rest_api_init', 'gutenberg_register_rest_customizer_nonces' ); + +- +-/** +- * Registers the Plugins REST API routes. +- */ +-function gutenberg_register_plugins_endpoint() { +- $plugins = new WP_REST_Plugins_Controller(); +- $plugins->register_routes(); +-} +-add_action( 'rest_api_init', 'gutenberg_register_plugins_endpoint' ); +- + /** + * Registers the Sidebars & Widgets REST API routes. + */ +@@ -308,25 +162,6 @@ function gutenberg_auto_draft_get_sample_permalink( $permalink, $id, $title, $na + } + add_filter( 'get_sample_permalink', 'gutenberg_auto_draft_get_sample_permalink', 10, 5 ); + +-/** +- * Registers the image editor. +- * +- * @since 7.x.0 +- */ +-function gutenberg_register_image_editor() { +- global $wp_version; +- +- // Strip '-src' from the version string. Messes up version_compare(). +- $version = str_replace( '-src', '', $wp_version ); +- +- // Only register routes for versions older than WP 5.5. +- if ( version_compare( $version, '5.5-beta', '<' ) ) { +- $image_editor = new WP_REST_Image_Editor_Controller(); +- $image_editor->register_routes(); +- } +-} +-add_filter( 'rest_api_init', 'gutenberg_register_image_editor' ); +- + /** + * Registers the post format search handler. + * +diff --git a/lib/template-loader.php b/lib/template-loader.php +deleted file mode 100644 +index b534db1637..0000000000 +--- a/lib/template-loader.php ++++ /dev/null +@@ -1,458 +0,0 @@ +- get_front_page_template. +- $template_hierarchy_filter = str_replace( '-', '', $template_type ) . '_template_hierarchy'; // front-page -> frontpage_template_hierarchy. +- +- $result = array(); +- $template_hierarchy_filter_function = function( $templates ) use ( &$result ) { +- $result = $templates; +- return $templates; +- }; +- +- add_filter( $template_hierarchy_filter, $template_hierarchy_filter_function, 20, 1 ); +- call_user_func( $get_template_function ); // This invokes template_hierarchy_filter. +- remove_filter( $template_hierarchy_filter, $template_hierarchy_filter_function, 20 ); +- +- return $result; +-} +- +-/** +- * Filters into the "{$type}_template" hooks to redirect them to the Full Site Editing template canvas. +- * +- * Internally, this communicates the block content that needs to be used by the template canvas through a global variable. +- * +- * @param string $template Path to the template. See locate_template(). +- * @param string $type Sanitized filename without extension. +- * @param array $templates A list of template candidates, in descending order of priority. +- * @return string The path to the Full Site Editing template canvas file. +- */ +-function gutenberg_override_query_template( $template, $type, array $templates = array() ) { +- global $_wp_current_template_content; +- +- $current_template = gutenberg_find_template_post_and_parts( $type, $templates ); +- +- if ( $current_template ) { +- $_wp_current_template_content = empty( $current_template['template_post']->post_content ) ? __( 'Empty template.', 'gutenberg' ) : $current_template['template_post']->post_content; +- +- if ( isset( $_GET['_wp-find-template'] ) ) { +- wp_send_json_success( $current_template['template_post'] ); +- } +- } else { +- if ( 'index' === $type ) { +- if ( isset( $_GET['_wp-find-template'] ) ) { +- wp_send_json_error( array( 'message' => __( 'No matching template found.', 'gutenberg' ) ) ); +- } +- } else { +- return false; // So that the template loader keeps looking for templates. +- } +- } +- +- // Add hooks for template canvas. +- // Add viewport meta tag. +- add_action( 'wp_head', 'gutenberg_viewport_meta_tag', 0 ); +- +- // Render title tag with content, regardless of whether theme has title-tag support. +- remove_action( 'wp_head', '_wp_render_title_tag', 1 ); // Remove conditional title tag rendering... +- add_action( 'wp_head', 'gutenberg_render_title_tag', 1 ); // ...and make it unconditional. +- +- // This file will be included instead of the theme's template file. +- return gutenberg_dir_path() . 'lib/template-canvas.php'; +-} +- +-/** +- * Recursively traverses a block tree, creating auto drafts +- * for any encountered template parts without a fixed post. +- * +- * @access private +- * +- * @param array $block The root block to start traversing from. +- * @return int[] A list of template parts IDs for the given block. +- */ +-function create_auto_draft_for_template_part_block( $block ) { +- $template_part_ids = array(); +- +- if ( 'core/template-part' === $block['blockName'] && isset( $block['attrs']['slug'] ) ) { +- if ( isset( $block['attrs']['postId'] ) ) { +- // Template part is customized. +- $template_part_id = $block['attrs']['postId']; +- } else { +- // A published post might already exist if this template part +- // was customized elsewhere or if it's part of a customized +- // template. We also check if an auto-draft was already created +- // because preloading can make this run twice, so, different code +- // paths can end up with different posts for the same template part. +- // E.g. The server could send back post ID 1 to the client, preload, +- // and create another auto-draft. So, if the client tries to resolve the +- // post ID from the slug and theme, it won't match with what the server sent. +- $template_part_query = new WP_Query( +- array( +- 'post_type' => 'wp_template_part', +- 'post_status' => array( 'publish', 'auto-draft' ), +- 'title' => $block['attrs']['slug'], +- 'meta_key' => 'theme', +- 'meta_value' => $block['attrs']['theme'], +- 'posts_per_page' => 1, +- 'no_found_rows' => true, +- ) +- ); +- $template_part_post = $template_part_query->have_posts() ? $template_part_query->next_post() : null; +- if ( $template_part_post && 'auto-draft' !== $template_part_post->post_status ) { +- $template_part_id = $template_part_post->ID; +- } else { +- // Template part is not customized, get it from a file and make an auto-draft for it, unless one already exists +- // and the underlying file hasn't changed. +- $template_part_file_path = +- get_stylesheet_directory() . '/block-template-parts/' . $block['attrs']['slug'] . '.html'; +- if ( ! file_exists( $template_part_file_path ) ) { +- if ( gutenberg_is_experiment_enabled( 'gutenberg-full-site-editing-demo' ) ) { +- $template_part_file_path = +- dirname( __FILE__ ) . '/demo-block-template-parts/' . $block['attrs']['slug'] . '.html'; +- if ( ! file_exists( $template_part_file_path ) ) { +- $template_part_file_path = false; +- } +- } else { +- $template_part_file_path = false; +- } +- } +- +- if ( $template_part_file_path ) { +- $file_contents = file_get_contents( $template_part_file_path ); +- if ( $template_part_post && $template_part_post->post_content === $file_contents ) { +- $template_part_id = $template_part_post->ID; +- } else { +- $template_part_id = wp_insert_post( +- array( +- 'post_content' => $file_contents, +- 'post_title' => $block['attrs']['slug'], +- 'post_status' => 'auto-draft', +- 'post_type' => 'wp_template_part', +- 'post_name' => $block['attrs']['slug'], +- 'meta_input' => array( +- 'theme' => $block['attrs']['theme'], +- ), +- ) +- ); +- } +- } +- } +- } +- $template_part_ids[ $block['attrs']['slug'] ] = $template_part_id; +- } +- +- foreach ( $block['innerBlocks'] as $inner_block ) { +- $template_part_ids = array_merge( $template_part_ids, create_auto_draft_for_template_part_block( $inner_block ) ); +- } +- return $template_part_ids; +-} +- +-/** +- * Return the correct 'wp_template' post and template part IDs for the current template. +- * +- * Accepts an optional $template_hierarchy argument as a hint. +- * +- * @param string $template_type The current template type. +- * @param string[] $template_hierarchy (optional) The current template hierarchy, ordered by priority. +- * @return null|array { +- * @type WP_Post|null template_post A template post object, or null if none could be found. +- * @type int[] A list of template parts IDs for the template. +- * } +- */ +-function gutenberg_find_template_post_and_parts( $template_type, $template_hierarchy = array() ) { +- if ( ! $template_type ) { +- return null; +- } +- +- if ( empty( $template_hierarchy ) ) { +- if ( 'index' === $template_type ) { +- $template_hierarchy = get_template_hierarchy( 'index' ); +- } else { +- $template_hierarchy = array_merge( get_template_hierarchy( $template_type ), get_template_hierarchy( 'index' ) ); +- } +- } +- +- $slugs = array_map( +- 'gutenberg_strip_php_suffix', +- $template_hierarchy +- ); +- +- // Find most specific 'wp_template' post matching the hierarchy. +- $template_query = new WP_Query( +- array( +- 'post_type' => 'wp_template', +- 'post_status' => 'publish', +- 'post_name__in' => $slugs, +- 'orderby' => 'post_name__in', +- 'posts_per_page' => 1, +- 'no_found_rows' => true, +- ) +- ); +- +- $current_template_post = $template_query->have_posts() ? $template_query->next_post() : null; +- +- // Build map of template slugs to their priority in the current hierarchy. +- $slug_priorities = array_flip( $slugs ); +- +- // See if there is a theme block template with higher priority than the resolved template post. +- $higher_priority_block_template_path = null; +- $higher_priority_block_template_priority = PHP_INT_MAX; +- $block_template_files = gutenberg_get_template_paths(); +- foreach ( $block_template_files as $path ) { +- if ( ! isset( $slug_priorities[ basename( $path, '.html' ) ] ) ) { +- continue; +- } +- $theme_block_template_priority = $slug_priorities[ basename( $path, '.html' ) ]; +- if ( +- $theme_block_template_priority < $higher_priority_block_template_priority && +- ( empty( $current_template_post ) || $theme_block_template_priority < $slug_priorities[ $current_template_post->post_name ] ) +- ) { +- $higher_priority_block_template_path = $path; +- $higher_priority_block_template_priority = $theme_block_template_priority; +- } +- } +- +- // If there is, use it instead. +- if ( isset( $higher_priority_block_template_path ) ) { +- $post_name = basename( $higher_priority_block_template_path, '.html' ); +- $file_contents = file_get_contents( $higher_priority_block_template_path ); +- $current_template_post = array( +- 'post_content' => $file_contents, +- 'post_title' => $post_name, +- 'post_status' => 'auto-draft', +- 'post_type' => 'wp_template', +- 'post_name' => $post_name, +- ); +- if ( is_admin() || defined( 'REST_REQUEST' ) ) { +- $template_query = new WP_Query( +- array( +- 'post_type' => 'wp_template', +- 'post_status' => 'auto-draft', +- 'name' => $post_name, +- 'posts_per_page' => 1, +- 'no_found_rows' => true, +- ) +- ); +- $current_template_post = $template_query->have_posts() ? $template_query->next_post() : $current_template_post; +- +- // Only create auto-draft of block template for editing +- // in admin screens, when necessary, because the underlying +- // file has changed. +- if ( is_array( $current_template_post ) || $current_template_post->post_content !== $file_contents ) { +- if ( ! is_array( $current_template_post ) ) { +- $current_template_post->post_content = $file_contents; +- } +- $current_template_post = get_post( +- wp_insert_post( $current_template_post ) +- ); +- } +- } else { +- $current_template_post = new WP_Post( +- (object) $current_template_post +- ); +- } +- } +- +- // If we haven't found any template post by here, it means that this theme doesn't even come with a fallback +- // `index.html` block template. We create one so that people that are trying to access the editor are greeted +- // with a blank page rather than an error. +- if ( ! $current_template_post && ( is_admin() || defined( 'REST_REQUEST' ) ) ) { +- $current_template_post = array( +- 'post_title' => 'index', +- 'post_status' => 'auto-draft', +- 'post_type' => 'wp_template', +- 'post_name' => 'index', +- ); +- $current_template_post = get_post( +- wp_insert_post( $current_template_post ) +- ); +- } +- +- if ( $current_template_post ) { +- $template_part_ids = array(); +- if ( is_admin() || defined( 'REST_REQUEST' ) ) { +- foreach ( parse_blocks( $current_template_post->post_content ) as $block ) { +- $template_part_ids = array_merge( $template_part_ids, create_auto_draft_for_template_part_block( $block ) ); +- } +- } +- return array( +- 'template_post' => $current_template_post, +- 'template_part_ids' => $template_part_ids, +- ); +- } +- return null; +-} +- +-/** +- * Displays title tag with content, regardless of whether theme has title-tag support. +- * +- * @see _wp_render_title_tag() +- */ +-function gutenberg_render_title_tag() { +- echo '' . wp_get_document_title() . '' . "\n"; +-} +- +-/** +- * Renders the markup for the current template. +- */ +-function gutenberg_render_the_template() { +- global $_wp_current_template_content; +- global $wp_embed; +- +- if ( ! $_wp_current_template_content ) { +- echo '

' . esc_html__( 'No matching template found', 'gutenberg' ) . '

'; +- return; +- } +- +- $content = $wp_embed->run_shortcode( $_wp_current_template_content ); +- $content = $wp_embed->autoembed( $content ); +- $content = do_blocks( $content ); +- $content = wptexturize( $content ); +- if ( function_exists( 'wp_filter_content_tags' ) ) { +- $content = wp_filter_content_tags( $content ); +- } else { +- $content = wp_make_content_images_responsive( $content ); +- } +- $content = str_replace( ']]>', ']]>', $content ); +- +- // Wrap block template in .wp-site-blocks to allow for specific descendant styles +- // (e.g. `.wp-site-blocks > *`). +- echo '
'; +- echo $content; // phpcs:ignore WordPress.Security.EscapeOutput +- echo '
'; +-} +- +-/** +- * Renders a 'viewport' meta tag. +- * +- * This is hooked into {@see 'wp_head'} to decouple its output from the default template canvas. +- */ +-function gutenberg_viewport_meta_tag() { +- echo '' . "\n"; +-} +- +-/** +- * Strips .php suffix from template file names. +- * +- * @access private +- * +- * @param string $template_file Template file name. +- * @return string Template file name without extension. +- */ +-function gutenberg_strip_php_suffix( $template_file ) { +- return preg_replace( '/\.php$/', '', $template_file ); +-} +- +-/** +- * Extends default editor settings to enable template and template part editing. +- * +- * @param array $settings Default editor settings. +- * +- * @return array Filtered editor settings. +- */ +-function gutenberg_template_loader_filter_block_editor_settings( $settings ) { +- global $post; +- +- if ( ! $post ) { +- return $settings; +- } +- +- // If this is the Site Editor, auto-drafts for template parts have already been generated +- // through `filter_rest_wp_template_part_query`, when called via the REST API. +- if ( isset( $settings['editSiteInitialState'] ) ) { +- return $settings; +- } +- +- // Otherwise, create template part auto-drafts for the edited post. +- $post = get_post(); +- foreach ( parse_blocks( $post->post_content ) as $block ) { +- create_auto_draft_for_template_part_block( $block ); +- } +- +- // TODO: Set editing mode and current template ID for editing modes support. +- return $settings; +-} +-add_filter( 'block_editor_settings', 'gutenberg_template_loader_filter_block_editor_settings' ); +- +-/** +- * Removes post details from block context when rendering a block template. +- * +- * @param array $context Default context. +- * +- * @return array Filtered context. +- */ +-function gutenberg_template_render_without_post_block_context( $context ) { +- /* +- * When loading a template or template part directly and not through a page +- * that resolves it, the top-level post ID and type context get set to that +- * of the template part. Templates are just the structure of a site, and +- * they should not be available as post context because blocks like Post +- * Content would recurse infinitely. +- */ +- if ( isset( $context['postType'] ) && +- ( 'wp_template' === $context['postType'] || 'wp_template_part' === $context['postType'] ) ) { +- unset( $context['postId'] ); +- unset( $context['postType'] ); +- } +- +- return $context; +-} +-add_filter( 'render_block_context', 'gutenberg_template_render_without_post_block_context' ); +diff --git a/lib/template-parts.php b/lib/template-parts.php +deleted file mode 100644 +index 9fe0fa530c..0000000000 +--- a/lib/template-parts.php ++++ /dev/null +@@ -1,260 +0,0 @@ +- __( 'Template Parts', 'gutenberg' ), +- 'singular_name' => __( 'Template Part', 'gutenberg' ), +- 'menu_name' => _x( 'Template Parts', 'Admin Menu text', 'gutenberg' ), +- 'add_new' => _x( 'Add New', 'Template Part', 'gutenberg' ), +- 'add_new_item' => __( 'Add New Template Part', 'gutenberg' ), +- 'new_item' => __( 'New Template Part', 'gutenberg' ), +- 'edit_item' => __( 'Edit Template Part', 'gutenberg' ), +- 'view_item' => __( 'View Template Part', 'gutenberg' ), +- 'all_items' => __( 'All Template Parts', 'gutenberg' ), +- 'search_items' => __( 'Search Template Parts', 'gutenberg' ), +- 'parent_item_colon' => __( 'Parent Template Part:', 'gutenberg' ), +- 'not_found' => __( 'No template parts found.', 'gutenberg' ), +- 'not_found_in_trash' => __( 'No template parts found in Trash.', 'gutenberg' ), +- 'archives' => __( 'Template part archives', 'gutenberg' ), +- 'insert_into_item' => __( 'Insert into template part', 'gutenberg' ), +- 'uploaded_to_this_item' => __( 'Uploaded to this template part', 'gutenberg' ), +- 'filter_items_list' => __( 'Filter template parts list', 'gutenberg' ), +- 'items_list_navigation' => __( 'Template parts list navigation', 'gutenberg' ), +- 'items_list' => __( 'Template parts list', 'gutenberg' ), +- ); +- +- $args = array( +- 'labels' => $labels, +- 'description' => __( 'Template parts to include in your templates.', 'gutenberg' ), +- 'public' => false, +- 'has_archive' => false, +- 'show_ui' => true, +- 'show_in_menu' => 'themes.php', +- 'show_in_admin_bar' => false, +- 'show_in_rest' => true, +- 'rest_base' => 'template-parts', +- 'map_meta_cap' => true, +- 'supports' => array( +- 'title', +- 'slug', +- 'editor', +- 'revisions', +- 'custom-fields', +- ), +- ); +- +- $meta_args = array( +- 'object_subtype' => 'wp_template_part', +- 'type' => 'string', +- 'description' => 'The theme that provided the template part, if any.', +- 'single' => true, +- 'show_in_rest' => true, +- ); +- +- register_post_type( 'wp_template_part', $args ); +- register_meta( 'post', 'theme', $meta_args ); +-} +-add_action( 'init', 'gutenberg_register_template_part_post_type' ); +- +-/** +- * Filters `wp_template_part` posts slug resolution to bypass deduplication logic as +- * template part slugs should be unique. +- * +- * @param string $slug The resolved slug (post_name). +- * @param int $post_ID Post ID. +- * @param string $post_status No uniqueness checks are made if the post is still draft or pending. +- * @param string $post_type Post type. +- * @param int $post_parent Post parent ID. +- * @param int $original_slug The desired slug (post_name). +- * @return string The original, desired slug. +- */ +-function gutenberg_filter_wp_template_part_wp_unique_post_slug( $slug, $post_ID, $post_status, $post_type, $post_parent, $original_slug ) { +- if ( 'wp_template_part' === $post_type ) { +- return $original_slug; +- } +- return $slug; +-} +-add_filter( 'wp_unique_post_slug', 'gutenberg_filter_wp_template_part_wp_unique_post_slug', 10, 6 ); +- +-/** +- * Fixes the label of the 'wp_template_part' admin menu entry. +- */ +-function gutenberg_fix_template_part_admin_menu_entry() { +- global $submenu; +- if ( ! isset( $submenu['themes.php'] ) ) { +- return; +- } +- $post_type = get_post_type_object( 'wp_template_part' ); +- if ( ! $post_type ) { +- return; +- } +- foreach ( $submenu['themes.php'] as $key => $submenu_entry ) { +- if ( $post_type->labels->all_items === $submenu['themes.php'][ $key ][0] ) { +- $submenu['themes.php'][ $key ][0] = $post_type->labels->menu_name; // phpcs:ignore WordPress.WP.GlobalVariablesOverride +- break; +- } +- } +-} +-add_action( 'admin_menu', 'gutenberg_fix_template_part_admin_menu_entry' ); +- +-/** +- * Filters the 'wp_template_part' post type columns in the admin list table. +- * +- * @param array $columns Columns to display. +- * @return array Filtered $columns. +- */ +-function gutenberg_filter_template_part_list_table_columns( array $columns ) { +- $columns['slug'] = __( 'Slug', 'gutenberg' ); +- if ( isset( $columns['date'] ) ) { +- unset( $columns['date'] ); +- } +- return $columns; +-} +-add_filter( 'manage_wp_template_part_posts_columns', 'gutenberg_filter_template_part_list_table_columns' ); +- +-/** +- * Renders column content for the 'wp_template_part' post type list table. +- * +- * @param string $column_name Column name to render. +- * @param int $post_id Post ID. +- */ +-function gutenberg_render_template_part_list_table_column( $column_name, $post_id ) { +- if ( 'slug' !== $column_name ) { +- return; +- } +- $post = get_post( $post_id ); +- echo esc_html( $post->post_name ); +-} +-add_action( 'manage_wp_template_part_posts_custom_column', 'gutenberg_render_template_part_list_table_column', 10, 2 ); +- +- +-/** +- * Filter for adding a `resolved`, a `template`, and a `theme` parameter to `wp_template_part` queries. +- * +- * @param array $query_params The query parameters. +- * @return array Filtered $query_params. +- */ +-function filter_rest_wp_template_part_collection_params( $query_params ) { +- $query_params += array( +- 'resolved' => array( +- 'description' => __( 'Whether to filter for resolved template parts.', 'gutenberg' ), +- 'type' => 'boolean', +- ), +- 'template' => array( +- 'description' => __( 'The template slug for the template that the template part is used by.', 'gutenberg' ), +- 'type' => 'string', +- ), +- 'theme' => array( +- 'description' => __( 'The theme slug for the theme that created the template part.', 'gutenberg' ), +- 'type' => 'string', +- ), +- ); +- return $query_params; +-} +-add_filter( 'rest_wp_template_part_collection_params', 'filter_rest_wp_template_part_collection_params', 99, 1 ); +- +-/** +- * Filter for supporting the `resolved`, `template`, and `theme` parameters in `wp_template_part` queries. +- * +- * @param array $args The query arguments. +- * @param WP_REST_Request $request The request object. +- * @return array Filtered $args. +- */ +-function filter_rest_wp_template_part_query( $args, $request ) { +- /** +- * Unlike `filter_rest_wp_template_query`, we resolve queries also if there's only a `template` argument set. +- * The difference is that in the case of templates, we can use the `slug` field that already exists (as part +- * of the entities endpoint, wheras for template parts, we have to register the extra `template` argument), +- * so we need the `resolved` flag to convey the different semantics (only return 'resolved' templates that match +- * the `slug` vs return _all_ templates that match it (e.g. including all auto-drafts)). +- * +- * A template parts query with a `template` arg but not a `resolved` one is conceivable, but probably wouldn't be +- * very useful: It'd be all template parts for all templates matching that `template` slug (including auto-drafts etc). +- * +- * @see filter_rest_wp_template_query +- * @see filter_rest_wp_template_part_collection_params +- * @see https://github.com/WordPress/gutenberg/pull/21878#discussion_r436961706 +- */ +- if ( $request['resolved'] || $request['template'] ) { +- $template_part_ids = array( 0 ); // Return nothing by default (the 0 is needed for `post__in`). +- $template_types = $request['template'] ? array( $request['template'] ) : get_template_types(); +- +- foreach ( $template_types as $template_type ) { +- // Skip 'embed' for now because it is not a regular template type. +- if ( in_array( $template_type, array( 'embed' ), true ) ) { +- continue; +- } +- +- $current_template = gutenberg_find_template_post_and_parts( $template_type ); +- if ( isset( $current_template ) ) { +- $template_part_ids = $template_part_ids + $current_template['template_part_ids']; +- } +- } +- $args['post__in'] = $template_part_ids; +- $args['post_status'] = array( 'publish', 'auto-draft' ); +- } +- +- if ( $request['theme'] ) { +- $meta_query = isset( $args['meta_query'] ) ? $args['meta_query'] : array(); +- $meta_query[] = array( +- 'key' => 'theme', +- 'value' => $request['theme'], +- ); +- +- // Ensure auto-drafts of all theme supplied template parts are created. +- if ( wp_get_theme()->stylesheet === $request['theme'] ) { +- /** +- * Finds all nested template part file paths in a theme's directory. +- * +- * @param string $base_directory The theme's file path. +- * @return array $path_list A list of paths to all template part files. +- */ +- function get_template_part_paths( $base_directory ) { +- $path_list = array(); +- if ( file_exists( $base_directory . '/block-template-parts' ) ) { +- $nested_files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $base_directory . '/block-template-parts' ) ); +- $nested_html_files = new RegexIterator( $nested_files, '/^.+\.html$/i', RecursiveRegexIterator::GET_MATCH ); +- foreach ( $nested_html_files as $path => $file ) { +- $path_list[] = $path; +- } +- } +- return $path_list; +- } +- +- // Get file paths for all theme supplied template parts. +- $template_part_files = get_template_part_paths( get_stylesheet_directory() ); +- if ( is_child_theme() ) { +- $template_part_files = array_merge( $template_part_files, get_template_part_paths( get_template_directory() ) ); +- } +- // Build and save each template part. +- foreach ( $template_part_files as $template_part_file ) { +- $content = file_get_contents( $template_part_file ); +- // Infer slug from filepath. +- $slug = substr( +- $template_part_file, +- // Starting position of slug. +- strpos( $template_part_file, 'block-template-parts/' ) + 21, +- // Subtract ending '.html'. +- -5 +- ); +- // Wrap content with the template part block, parse, and create auto-draft. +- $template_part_string = '' . $content . ''; +- $template_part_block = parse_blocks( $template_part_string )[0]; +- create_auto_draft_for_template_part_block( $template_part_block ); +- } +- }; +- +- $args['meta_query'] = $meta_query; +- } +- +- return $args; +-} +-add_filter( 'rest_wp_template_part_query', 'filter_rest_wp_template_part_query', 99, 2 ); +diff --git a/lib/upgrade.php b/lib/upgrade.php +new file mode 100644 +index 0000000000..d930999a67 +--- /dev/null ++++ b/lib/upgrade.php +@@ -0,0 +1,65 @@ ++ array( 'auto-draft' ), ++ 'post_type' => array( 'wp_template', 'wp_template_part' ), ++ 'posts_per_page' => -1, ++ ) ++ ); ++ foreach ( $delete_query->get_posts() as $post ) { ++ wp_delete_post( $post->ID, true ); ++ } ++ ++ // Delete _wp_file_based term. ++ $term = get_term_by( 'name', '_wp_file_based', 'wp_theme' ); ++ if ( $term ) { ++ wp_delete_term( $term->term_id, 'wp_theme' ); ++ } ++ ++ // Delete useless options. ++ delete_option( 'gutenberg_last_synchronize_theme_template_checks' ); ++ delete_option( 'gutenberg_last_synchronize_theme_template-part_checks' ); ++} ++ ++// Deletion of the `_wp_file_based` term (in _gutenberg_migrate_remove_fse_drafts) must happen ++// after its taxonomy (`wp_theme`) is registered. This happens in `gutenberg_register_wp_theme_taxonomy`, ++// which is hooked into `init` (default priority, i.e. 10). ++add_action( 'init', '_gutenberg_migrate_database', 20 ); +diff --git a/lib/utils.php b/lib/utils.php +index b5cdef7f99..f3bedfba63 100644 +--- a/lib/utils.php ++++ b/lib/utils.php +@@ -31,3 +31,62 @@ function gutenberg_experimental_get( $array, $path, $default = array() ) { + } + return $array; + } ++ ++/** ++ * Sets an array in depth based on a path of keys. ++ * ++ * It is the PHP equivalent of JavaScript's `lodash.set()` and mirroring it may help other components ++ * retain some symmetry between client and server implementations. ++ * ++ * Example usage: ++ * ++ * $array = array(); ++ * _wp_array_set( $array, array( 'a', 'b', 'c', 1 ); ++ * $array becomes: ++ * array( ++ * 'a' => array( ++ * 'b' => array( ++ * 'c' => 1, ++ * ), ++ * ), ++ * ); ++ * ++ * @param array $array An array that we want to mutate to include a specific value in a path. ++ * @param array $path An array of keys describing the path that we want to mutate. ++ * @param mixed $value The value that will be set. ++ */ ++function gutenberg_experimental_set( &$array, $path, $value = null ) { ++ // Confirm $array is valid. ++ if ( ! is_array( $array ) ) { ++ return; ++ } ++ ++ // Confirm $path is valid. ++ if ( ! is_array( $path ) ) { ++ return; ++ } ++ $path_length = count( $path ); ++ if ( 0 === $path_length ) { ++ return; ++ } ++ foreach ( $path as $path_element ) { ++ if ( ++ ! is_string( $path_element ) && ! is_integer( $path_element ) && ++ ! is_null( $path_element ) ++ ) { ++ return; ++ } ++ } ++ ++ for ( $i = 0; $i < $path_length - 1; ++$i ) { ++ $path_element = $path[ $i ]; ++ if ( ++ ! array_key_exists( $path_element, $array ) || ++ ! is_array( $array[ $path_element ] ) ++ ) { ++ $array[ $path_element ] = array(); ++ } ++ $array = &$array[ $path_element ]; ++ } ++ $array[ $path[ $i ] ] = $value; ++} +diff --git a/lib/widgets-page.php b/lib/widgets-page.php +index f6d31ca5b7..d31b814c72 100644 +--- a/lib/widgets-page.php ++++ b/lib/widgets-page.php +@@ -43,37 +43,8 @@ function gutenberg_widgets_init( $hook ) { + + $initializer_name = 'initialize'; + +- // Media settings. +- $max_upload_size = wp_max_upload_size(); +- if ( ! $max_upload_size ) { +- $max_upload_size = 0; +- } +- +- /** This filter is documented in wp-admin/includes/media.php */ +- $image_size_names = apply_filters( +- 'image_size_names_choose', +- array( +- 'thumbnail' => __( 'Thumbnail', 'gutenberg' ), +- 'medium' => __( 'Medium', 'gutenberg' ), +- 'large' => __( 'Large', 'gutenberg' ), +- 'full' => __( 'Full Size', 'gutenberg' ), +- ) +- ); +- +- $available_image_sizes = array(); +- foreach ( $image_size_names as $image_size_slug => $image_size_name ) { +- $available_image_sizes[] = array( +- 'slug' => $image_size_slug, +- 'name' => $image_size_name, +- ); +- } +- + $settings = array_merge( +- array( +- 'imageSizes' => $available_image_sizes, +- 'isRTL' => is_rtl(), +- 'maxUploadFileSize' => $max_upload_size, +- ), ++ gutenberg_get_common_block_editor_settings(), + gutenberg_get_legacy_widget_settings() + ); + +@@ -109,7 +80,7 @@ function gutenberg_widgets_init( $hook ) { + wp.editWidgets.%s( "widgets-editor", %s ); + } );', + $initializer_name, +- wp_json_encode( gutenberg_experiments_editor_settings( $settings ) ) ++ wp_json_encode( $settings ) + ) + ); + +@@ -143,3 +114,15 @@ function gutenberg_widgets_editor_load_block_editor_scripts_and_styles( $is_bloc + + add_filter( 'should_load_block_editor_scripts_and_styles', 'gutenberg_widgets_editor_load_block_editor_scripts_and_styles' ); + ++/** ++ * Show responsive embeds correctly on the widgets screen by adding the wp-embed-responsive class. ++ * ++ * @param string $classes existing admin body classes. ++ * ++ * @return string admin body classes including the wp-embed-responsive class. ++ */ ++function gutenberg_widgets_editor_add_responsive_embed_body_class( $classes ) { ++ return "$classes wp-embed-responsive"; ++} ++ ++add_filter( 'admin_body_class', 'gutenberg_widgets_editor_add_responsive_embed_body_class' ); +diff --git a/lib/widgets.php b/lib/widgets.php +index f87867b2b4..4236a66413 100644 +--- a/lib/widgets.php ++++ b/lib/widgets.php +@@ -200,7 +200,7 @@ function gutenberg_get_legacy_widget_settings() { + + $settings['availableLegacyWidgets'] = $available_legacy_widgets; + +- return gutenberg_experiments_editor_settings( $settings ); ++ return $settings; + } + + /** +@@ -278,7 +278,7 @@ function gutenberg_load_widget_preview_if_requested() { + current_user_can( 'edit_theme_options' ) + ) { + define( 'IFRAME_REQUEST', true ); +- require_once dirname( __FILE__ ) . '/widget-preview-template.php'; ++ require_once __DIR__ . '/widget-preview-template.php'; + exit; + } + } diff --git a/packages/e2e-tests/specs/widgets/adding-widgets.test.js b/packages/e2e-tests/specs/widgets/adding-widgets.test.js index 3b56f71b822258..4f2244ff08c279 100644 --- a/packages/e2e-tests/specs/widgets/adding-widgets.test.js +++ b/packages/e2e-tests/specs/widgets/adding-widgets.test.js @@ -6,6 +6,7 @@ import { deactivatePlugin, activatePlugin, activateTheme, + pressKeyWithModifier, } from '@wordpress/e2e-test-utils'; /**