From 9a604011ee7d16a498797b2106860eb1afc3e940 Mon Sep 17 00:00:00 2001 From: Timothy Jacobs Date: Thu, 25 Jun 2020 22:11:09 +0000 Subject: [PATCH] Themes: Introduce register_theme_feature API. Currently themes can declare support for a given feature by using add_theme_support(). This commit adds a register_theme_feature() API that allows plugins and WordPress Core to declare a list of available features that themes can support. The REST API uses this to expose a theme's supported features if the feature has been registered with "show_in_rest" set to true. Props kadamwhite, spacedmonkey, williampatton, desrosj, TimothyBlynJacobs. Fixes #49406. git-svn-id: https://develop.svn.wordpress.org/trunk@48171 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/default-filters.php | 1 + src/wp-includes/rest-api.php | 32 ++ .../class-wp-rest-themes-controller.php | 361 +++--------- src/wp-includes/theme.php | 518 ++++++++++++++++++ .../tests/rest-api/rest-themes-controller.php | 28 +- tests/phpunit/tests/theme.php | 284 ++++++++++ 6 files changed, 929 insertions(+), 295 deletions(-) diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 18c91c0a4e..0a5a46695a 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -470,6 +470,7 @@ add_action( 'transition_post_status', '_wp_customize_publish_changeset', 10, 3 ) add_action( 'admin_enqueue_scripts', '_wp_customize_loader_settings' ); add_action( 'delete_attachment', '_delete_attachment_theme_mod' ); add_action( 'transition_post_status', '_wp_keep_alive_customize_changeset_dependent_auto_drafts', 20, 3 ); +add_action( 'setup_theme', 'create_initial_theme_features', 0 ); // Calendar widget cache. add_action( 'save_post', 'delete_get_calendar_cache' ); diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index ac422b27aa..43569e3c1a 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -1779,3 +1779,35 @@ function rest_filter_response_by_context( $data, $schema, $context ) { return $data; } + +/** + * Sets the "additionalProperties" to false by default for all object definitions in the schema. + * + * @since 5.5.0 + * + * @param array $schema The schema to modify. + * @return array The modified schema. + */ +function rest_default_additional_properties_to_false( $schema ) { + $type = (array) $schema['type']; + + if ( in_array( 'object', $type, true ) ) { + if ( isset( $schema['properties'] ) ) { + foreach ( $schema['properties'] as $key => $child_schema ) { + $schema['properties'][ $key ] = rest_default_additional_properties_to_false( $child_schema ); + } + } + + if ( ! isset( $schema['additionalProperties'] ) ) { + $schema['additionalProperties'] = false; + } + } + + if ( in_array( 'array', $type, true ) ) { + if ( isset( $schema['items'] ) ) { + $schema['items'] = rest_default_additional_properties_to_false( $schema['items'] ); + } + } + + return $schema; +} diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-themes-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-themes-controller.php index f5aed07b64..62b83ba90b 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-themes-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-themes-controller.php @@ -167,50 +167,38 @@ class WP_REST_Themes_Controller extends WP_REST_Controller { } if ( rest_is_field_included( 'theme_supports', $fields ) ) { - $item_schemas = $this->get_item_schema(); - $theme_supports = $item_schemas['properties']['theme_supports']['properties']; - foreach ( $theme_supports as $name => $schema ) { + foreach ( get_registered_theme_features() as $feature => $config ) { + if ( ! is_array( $config['show_in_rest'] ) ) { + continue; + } + + $name = $config['show_in_rest']['name']; + if ( ! rest_is_field_included( "theme_supports.{$name}", $fields ) ) { continue; } - if ( 'formats' === $name ) { + if ( ! current_theme_supports( $feature ) ) { + $data['theme_supports'][ $name ] = $config['show_in_rest']['schema']['default']; continue; } - if ( ! current_theme_supports( $name ) ) { - $data['theme_supports'][ $name ] = false; + $support = get_theme_support( $feature ); + + if ( isset( $config['show_in_rest']['prepare_callback'] ) ) { + $prepare = $config['show_in_rest']['prepare_callback']; + } else { + $prepare = array( $this, 'prepare_theme_support' ); + } + + $prepared = $prepare( $support, $config, $feature, $request ); + + if ( is_wp_error( $prepared ) ) { continue; } - if ( 'boolean' === $schema['type'] ) { - $data['theme_supports'][ $name ] = true; - continue; - } - - $support = get_theme_support( $name ); - - if ( is_array( $support ) ) { - // None of the Core theme supports have variadic args. - $support = $support[0]; - - // Core multi-type theme-support schema definitions always list boolean first. - if ( is_array( $schema['type'] ) && 'boolean' === $schema['type'][0] ) { - // Pass the non-boolean type through to the sanitizer, which cannot itself - // determine the intended type if the value is invalid (for example if an - // object includes non-safelisted properties). - $schema['type'] = $schema['type'][1]; - } - } - - $data['theme_supports'][ $name ] = rest_sanitize_value_from_schema( $support, $schema ); + $data['theme_supports'][ $name ] = $prepared; } - - $formats = get_theme_support( 'post-formats' ); - $formats = is_array( $formats ) ? array_values( $formats[0] ) : array(); - $formats = array_merge( array( 'standard' ), $formats ); - - $data['theme_supports']['formats'] = $formats; } $data = $this->add_additional_fields_to_object( $data, $request ); @@ -230,6 +218,41 @@ class WP_REST_Themes_Controller extends WP_REST_Controller { return apply_filters( 'rest_prepare_theme', $response, $theme, $request ); } + /** + * Prepares the theme support value for inclusion in the REST API response. + * + * @since 5.5.0 + * + * @param mixed $support The raw value from {@see get_theme_support()} + * @param array $args The feature's registration args. + * @param string $feature The feature name. + * @param WP_REST_Request $request The request object. + * @return mixed The prepared support value. + */ + protected function prepare_theme_support( $support, $args, $feature, $request ) { + $schema = $args['show_in_rest']['schema']; + + if ( 'boolean' === $schema['type'] ) { + return true; + } + + if ( is_array( $support ) ) { + if ( ! $args['variadic'] ) { + $support = $support[0]; + } + + // Multi-type theme-support schema definitions always list boolean first. + if ( is_array( $schema['type'] ) && 'boolean' === $schema['type'][0] ) { + // Pass the non-boolean type through to the sanitizer, which cannot itself + // determine the intended type if the value is invalid (for example if an + // object includes non-safelisted properties). See #50300. + $schema['type'] = $schema['type'][1]; + } + } + + return rest_sanitize_value_from_schema( $support, $schema ); + } + /** * Retrieves the theme's schema, conforming to JSON Schema. * @@ -362,267 +385,7 @@ class WP_REST_Themes_Controller extends WP_REST_Controller { 'description' => __( 'Features supported by this theme.' ), 'type' => 'object', 'readonly' => true, - 'properties' => array( - 'align-wide' => array( - 'description' => __( 'Whether theme opts in to wide alignment CSS class.' ), - 'type' => 'boolean', - ), - 'automatic-feed-links' => array( - 'description' => __( 'Whether posts and comments RSS feed links are added to head.' ), - 'type' => 'boolean', - ), - 'custom-header' => array( - 'description' => __( 'Custom header if defined by the theme.' ), - 'type' => array( 'boolean', 'object' ), - 'properties' => array( - 'default-image' => array( - 'type' => 'string', - 'format' => 'uri', - ), - 'random-default' => array( - 'type' => 'boolean', - ), - 'width' => array( - 'type' => 'integer', - ), - 'height' => array( - 'type' => 'integer', - ), - 'flex-height' => array( - 'type' => 'boolean', - ), - 'flex-width' => array( - 'type' => 'boolean', - ), - 'default-text-color' => array( - 'type' => 'string', - ), - 'header-text' => array( - 'type' => 'boolean', - ), - 'uploads' => array( - 'type' => 'boolean', - ), - 'video' => array( - 'type' => 'boolean', - ), - ), - 'additionalProperties' => false, - ), - 'custom-background' => array( - 'description' => __( 'Custom background if defined by the theme.' ), - 'type' => array( 'boolean', 'object' ), - 'properties' => array( - 'default-image' => array( - 'type' => 'string', - 'format' => 'uri', - ), - 'default-preset' => array( - 'type' => 'string', - 'enum' => array( - 'default', - 'fill', - 'fit', - 'repeat', - 'custom', - ), - ), - 'default-position-x' => array( - 'type' => 'string', - 'enum' => array( - 'left', - 'center', - 'right', - ), - ), - 'default-position-y' => array( - 'type' => 'string', - 'enum' => array( - 'left', - 'center', - 'right', - ), - ), - 'default-size' => array( - 'type' => 'string', - 'enum' => array( - 'auto', - 'contain', - 'cover', - ), - ), - 'default-repeat' => array( - 'type' => 'string', - 'enum' => array( - 'repeat-x', - 'repeat-y', - 'repeat', - 'no-repeat', - ), - ), - 'default-attachment' => array( - 'type' => 'string', - 'enum' => array( - 'scroll', - 'fixed', - ), - ), - 'default-color' => array( - 'type' => 'string', - ), - ), - 'additionalProperties' => false, - ), - 'custom-logo' => array( - 'description' => __( 'Custom logo if defined by the theme.' ), - 'type' => array( 'boolean', 'object' ), - 'properties' => array( - 'width' => array( - 'type' => 'integer', - ), - 'height' => array( - 'type' => 'integer', - ), - 'flex-width' => array( - 'type' => 'boolean', - ), - 'flex-height' => array( - 'type' => 'boolean', - ), - 'header-text' => array( - 'type' => 'array', - 'items' => array( - 'type' => 'string', - ), - ), - ), - 'additionalProperties' => false, - ), - 'customize-selective-refresh-widgets' => array( - 'description' => __( 'Whether the theme enables Selective Refresh for Widgets being managed with the Customizer.' ), - 'type' => 'boolean', - ), - 'dark-editor-style' => array( - 'description' => __( 'Whether theme opts in to the dark editor style UI.' ), - 'type' => 'boolean', - ), - 'disable-custom-colors' => array( - 'description' => __( 'Whether the theme disables custom colors.' ), - 'type' => 'boolean', - ), - 'disable-custom-font-sizes' => array( - 'description' => __( 'Whether the theme disables custom font sizes.' ), - 'type' => 'boolean', - ), - 'disable-custom-gradients' => array( - 'description' => __( 'Whether the theme disables custom gradients.' ), - 'type' => 'boolean', - ), - 'editor-color-palette' => array( - 'description' => __( 'Custom color palette if defined by the theme.' ), - 'type' => array( 'boolean', 'array' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'name' => array( - 'type' => 'string', - ), - 'slug' => array( - 'type' => 'string', - ), - 'color' => array( - 'type' => 'string', - ), - ), - 'additionalProperties' => false, - ), - ), - 'editor-font-sizes' => array( - 'description' => __( 'Custom font sizes if defined by the theme.' ), - 'type' => array( 'boolean', 'array' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'name' => array( - 'type' => 'string', - ), - 'size' => array( - 'type' => 'number', - ), - 'slug' => array( - 'type' => 'string', - ), - ), - 'additionalProperties' => false, - ), - ), - 'editor-gradient-presets' => array( - 'description' => __( 'Custom gradient presets if defined by the theme.' ), - 'type' => array( 'boolean', 'array' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'name' => array( - 'type' => 'string', - ), - 'gradient' => array( - 'type' => 'string', - ), - 'slug' => array( - 'type' => 'string', - ), - ), - 'additionalProperties' => false, - ), - ), - 'editor-styles' => array( - 'description' => __( 'Whether theme opts in to the editor styles CSS wrapper.' ), - 'type' => 'boolean', - ), - 'formats' => array( - 'description' => __( 'Post formats supported.' ), - 'type' => 'array', - 'items' => array( - 'type' => 'string', - 'enum' => get_post_format_slugs(), - ), - ), - 'html5' => array( - 'description' => __( 'Allows use of html5 markup for search forms, comment forms, comment lists, gallery, and caption.' ), - 'type' => array( 'boolean', 'array' ), - 'items' => array( - 'type' => 'string', - 'enum' => array( - 'search-form', - 'comment-form', - 'comment-list', - 'gallery', - 'caption', - 'script', - 'style', - ), - ), - ), - 'post-thumbnails' => array( - 'description' => __( 'Whether the theme supports post thumbnails.' ), - 'type' => array( 'boolean', 'array' ), - 'items' => array( - 'type' => 'string', - ), - ), - 'responsive-embeds' => array( - 'description' => __( 'Whether the theme supports responsive embedded content.' ), - 'type' => 'boolean', - ), - 'title-tag' => array( - 'description' => __( 'Whether the theme can manage the document title tag.' ), - 'type' => 'boolean', - ), - 'wp-block-styles' => array( - 'description' => __( 'Whether theme opts in to default WordPress block styles for viewing.' ), - 'type' => 'boolean', - ), - ), + 'properties' => array(), ), 'theme_uri' => array( 'description' => __( 'The URI of the theme\'s webpage.' ), @@ -649,6 +412,16 @@ class WP_REST_Themes_Controller extends WP_REST_Controller { ), ); + foreach ( get_registered_theme_features() as $feature => $config ) { + if ( ! is_array( $config['show_in_rest'] ) ) { + continue; + } + + $name = $config['show_in_rest']['name']; + + $schema['properties']['theme_supports']['properties'][ $name ] = $config['show_in_rest']['schema']; + } + $this->schema = $schema; return $this->add_additional_fields_schema( $this->schema ); diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php index 80b0f1c072..baf1ea2157 100644 --- a/src/wp-includes/theme.php +++ b/src/wp-includes/theme.php @@ -2993,6 +2993,156 @@ function require_if_theme_supports( $feature, $include ) { return false; } +/** + * Registers a theme feature for use in {@see add_theme_support}. + * + * This does not indicate that the current theme supports the feature, it only describes the feature's supported options. + * + * @since 5.5.0 + * + * @global $_wp_registered_theme_features + * + * @param string $feature The name uniquely identifying the feature. + * @param array $args { + * Data used to describe the theme + * + * @type string $type The type of data associated with this feature. Defaults to 'boolean'. + * Valid values are 'string', 'boolean', 'integer', 'number', 'array', and 'object'. + * @type boolean $variadic Does this feature utilize the variadic support of {@see add_theme_support()}, + * or are all arguments specified as the second parameter. Must be used with the "array" type. + * @type string $description A short description of the feature. Included in the Themes REST API schema. Intended for developers. + * @type bool|array $show_in_rest { + * Whether this feature should be included in the Themes REST API endpoint. Defaults to not being included. + * When registering an 'array' or 'object' type, this argument must be an array with the 'schema' key. + * + * @type array $schema Specifies the JSON Schema definition describing the feature. If any objects in the schema + * do not include the 'additionalProperties' keyword, it is set to false. + * @type string $name An alternate name to be use as the property name in the REST API. + * @type callable $prepare_callback A function used to format the theme support in the REST API. Receives the raw theme support value. + * } + * } + * @return true|WP_Error True if the theme feature was successfully registered, a WP_Error object if not. + */ +function register_theme_feature( $feature, $args = array() ) { + global $_wp_registered_theme_features; + + if ( ! is_array( $_wp_registered_theme_features ) ) { + $_wp_registered_theme_features = array(); + } + + $defaults = array( + 'type' => 'boolean', + 'variadic' => false, + 'description' => '', + 'show_in_rest' => false, + ); + + $args = wp_parse_args( $args, $defaults ); + + if ( true === $args['show_in_rest'] ) { + $args['show_in_rest'] = array(); + } + + if ( is_array( $args['show_in_rest'] ) ) { + $args['show_in_rest'] = wp_parse_args( + $args['show_in_rest'], + array( + 'schema' => array(), + 'name' => $feature, + 'prepare_callback' => null, + ) + ); + } + + if ( ! in_array( $args['type'], array( 'string', 'boolean', 'integer', 'number', 'array', 'object' ), true ) ) { + return new WP_Error( 'invalid_type', __( 'The feature "type" is not valid JSON Schema type.' ) ); + } + + if ( true === $args['variadic'] && 'array' !== $args['type'] ) { + return new WP_Error( 'variadic_must_be_array', __( 'When registering a "variadic" theme feature, the "type" must be an "array".' ) ); + } + + if ( false !== $args['show_in_rest'] && in_array( $args['type'], array( 'array', 'object' ), true ) ) { + if ( ! is_array( $args['show_in_rest'] ) || empty( $args['show_in_rest']['schema'] ) ) { + return new WP_Error( 'missing_schema', __( 'When registering an "array" or "object" feature to show in the REST API, the feature\'s schema must also be defined.' ) ); + } + + if ( 'array' === $args['type'] && ! isset( $args['show_in_rest']['schema']['items'] ) ) { + return new WP_Error( 'missing_schema_items', __( 'When registering an "array" feature, the feature\'s schema must include the "items" keyword.' ) ); + } + + if ( 'object' === $args['type'] && ! isset( $args['show_in_rest']['schema']['properties'] ) ) { + return new WP_Error( 'missing_schema_properties', __( 'When registering an "object" feature, the feature\'s schema must include the "properties" keyword.' ) ); + } + } + + if ( is_array( $args['show_in_rest'] ) ) { + if ( isset( $args['show_in_rest']['prepare_callback'] ) && ! is_callable( $args['show_in_rest']['prepare_callback'] ) ) { + return new WP_Error( 'invalid_rest_prepare_callback', __( 'The prepare_callback must be a callable function.' ) ); + } + + $args['show_in_rest']['schema'] = wp_parse_args( + $args['show_in_rest']['schema'], + array( + 'description' => $args['description'], + 'type' => $args['type'], + 'default' => false, + ) + ); + + if ( is_bool( $args['show_in_rest']['schema']['default'] ) && ! in_array( 'boolean', (array) $args['show_in_rest']['schema']['type'], true ) ) { + // Automatically include the "boolean" type when the default value is a boolean. + $args['show_in_rest']['schema']['type'] = (array) $args['show_in_rest']['schema']['type']; + array_unshift( $args['show_in_rest']['schema']['type'], 'boolean' ); + } + + $args['show_in_rest']['schema'] = rest_default_additional_properties_to_false( $args['show_in_rest']['schema'] ); + } + + $_wp_registered_theme_features[ $feature ] = $args; + + return true; +} + +/** + * Gets the list of registered theme features. + * + * @since 5.5.0 + * + * @global $_wp_registered_theme_features + * + * @return array[] List of theme features, keyed by their name. + */ +function get_registered_theme_features() { + global $_wp_registered_theme_features; + + if ( ! is_array( $_wp_registered_theme_features ) ) { + return array(); + } + + return $_wp_registered_theme_features; +} + +/** + * Gets the registration config for a theme feature. + * + * @since 5.5.0 + * + * @global $_wp_registered_theme_features + * + * @param string $feature The feature name. + * @return array|null The registration args, or null if the feature was not registered. + */ +function get_registered_theme_feature( $feature ) { + global $_wp_registered_theme_features; + + if ( ! is_array( $_wp_registered_theme_features ) ) { + return null; + } + + return isset( $_wp_registered_theme_features[ $feature ] ) ? $_wp_registered_theme_features[ $feature ] : null; +} + /** * Checks an attachment being deleted to see if it's a header or background image. * @@ -3462,3 +3612,371 @@ function _wp_keep_alive_customize_changeset_dependent_auto_drafts( $new_status, clean_post_cache( $post_id ); } } + +/** + * Creates the initial theme features when the 'setup_theme' action is fired. + * + * See {@see 'setup_theme'}. + * + * @since 5.5.0 + */ +function create_initial_theme_features() { + register_theme_feature( + 'align-wide', + array( + 'description' => __( 'Whether theme opts in to wide alignment CSS class.' ), + 'show_in_rest' => true, + ) + ); + register_theme_feature( + 'automatic-feed-links', + array( + 'description' => __( 'Whether posts and comments RSS feed links are added to head.' ), + 'show_in_rest' => true, + ) + ); + register_theme_feature( + 'custom-background', + array( + 'description' => __( 'Custom background if defined by the theme.' ), + 'type' => 'object', + 'show_in_rest' => array( + 'schema' => array( + 'properties' => array( + 'default-image' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'default-preset' => array( + 'type' => 'string', + 'enum' => array( + 'default', + 'fill', + 'fit', + 'repeat', + 'custom', + ), + ), + 'default-position-x' => array( + 'type' => 'string', + 'enum' => array( + 'left', + 'center', + 'right', + ), + ), + 'default-position-y' => array( + 'type' => 'string', + 'enum' => array( + 'left', + 'center', + 'right', + ), + ), + 'default-size' => array( + 'type' => 'string', + 'enum' => array( + 'auto', + 'contain', + 'cover', + ), + ), + 'default-repeat' => array( + 'type' => 'string', + 'enum' => array( + 'repeat-x', + 'repeat-y', + 'repeat', + 'no-repeat', + ), + ), + 'default-attachment' => array( + 'type' => 'string', + 'enum' => array( + 'scroll', + 'fixed', + ), + ), + 'default-color' => array( + 'type' => 'string', + ), + ), + ), + ), + ) + ); + register_theme_feature( + 'custom-header', + array( + 'description' => __( 'Custom header if defined by the theme.' ), + 'type' => 'object', + 'show_in_rest' => array( + 'schema' => array( + 'properties' => array( + 'default-image' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'random-default' => array( + 'type' => 'boolean', + ), + 'width' => array( + 'type' => 'integer', + ), + 'height' => array( + 'type' => 'integer', + ), + 'flex-height' => array( + 'type' => 'boolean', + ), + 'flex-width' => array( + 'type' => 'boolean', + ), + 'default-text-color' => array( + 'type' => 'string', + ), + 'header-text' => array( + 'type' => 'boolean', + ), + 'uploads' => array( + 'type' => 'boolean', + ), + 'video' => array( + 'type' => 'boolean', + ), + ), + ), + ), + ) + ); + register_theme_feature( + 'custom-logo', + array( + 'type' => 'object', + 'description' => __( 'Custom logo if defined by the theme.' ), + 'show_in_rest' => array( + 'schema' => array( + 'properties' => array( + 'width' => array( + 'type' => 'integer', + ), + 'height' => array( + 'type' => 'integer', + ), + 'flex-width' => array( + 'type' => 'boolean', + ), + 'flex-height' => array( + 'type' => 'boolean', + ), + 'header-text' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + ), + ), + ), + ) + ); + register_theme_feature( + 'customize-selective-refresh-widgets', + array( + 'description' => __( 'Whether the theme enables Selective Refresh for Widgets being managed with the Customizer.' ), + 'show_in_rest' => true, + ) + ); + register_theme_feature( + 'dark-editor-style', + array( + 'description' => __( 'Whether theme opts in to the dark editor style UI.' ), + 'show_in_rest' => true, + ) + ); + register_theme_feature( + 'disable-custom-colors', + array( + 'description' => __( 'Whether the theme disables custom colors.' ), + 'show_in_rest' => true, + ) + ); + register_theme_feature( + 'disable-custom-font-sizes', + array( + 'description' => __( 'Whether the theme disables custom font sizes.' ), + 'show_in_rest' => true, + ) + ); + register_theme_feature( + 'disable-custom-gradients', + array( + 'description' => __( 'Whether the theme disables custom gradients.' ), + 'show_in_rest' => true, + ) + ); + register_theme_feature( + 'editor-color-palette', + array( + 'type' => 'array', + 'description' => __( 'Custom color palette if defined by the theme.' ), + 'show_in_rest' => array( + 'schema' => array( + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'name' => array( + 'type' => 'string', + ), + 'slug' => array( + 'type' => 'string', + ), + 'color' => array( + 'type' => 'string', + ), + ), + ), + ), + ), + ) + ); + register_theme_feature( + 'editor-font-sizes', + array( + 'type' => 'array', + 'description' => __( 'Custom font sizes if defined by the theme.' ), + 'show_in_rest' => array( + 'schema' => array( + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'name' => array( + 'type' => 'string', + ), + 'size' => array( + 'type' => 'number', + ), + 'slug' => array( + 'type' => 'string', + ), + ), + ), + ), + ), + ) + ); + register_theme_feature( + 'editor-gradient-presets', + array( + 'type' => 'array', + 'description' => __( 'Custom gradient presets if defined by the theme.' ), + 'show_in_rest' => array( + 'schema' => array( + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'name' => array( + 'type' => 'string', + ), + 'gradient' => array( + 'type' => 'string', + ), + 'slug' => array( + 'type' => 'string', + ), + ), + ), + ), + ), + ) + ); + register_theme_feature( + 'editor-styles', + array( + 'description' => __( 'Whether theme opts in to the editor styles CSS wrapper.' ), + 'show_in_rest' => true, + ) + ); + register_theme_feature( + 'html5', + array( + 'type' => 'array', + 'description' => __( 'Allows use of HTML5 markup for search forms, comment forms, comment lists, gallery, and caption.' ), + 'show_in_rest' => array( + 'schema' => array( + 'items' => array( + 'type' => 'string', + 'enum' => array( + 'search-form', + 'comment-form', + 'comment-list', + 'gallery', + 'caption', + 'script', + 'style', + ), + ), + ), + ), + ) + ); + register_theme_feature( + 'post-formats', + array( + 'type' => 'array', + 'description' => __( 'Post formats supported.' ), + 'show_in_rest' => array( + 'name' => 'formats', + 'schema' => array( + 'items' => array( + 'type' => 'string', + 'enum' => get_post_format_slugs(), + ), + 'default' => array( 'standard' ), + ), + 'prepare_callback' => static function ( $formats ) { + $formats = is_array( $formats ) ? array_values( $formats[0] ) : array(); + $formats = array_merge( array( 'standard' ), $formats ); + + return $formats; + }, + ), + ) + ); + register_theme_feature( + 'post-thumbnails', + array( + 'type' => 'array', + 'description' => __( 'The post types that support thumbnails or true if all post types are supported.' ), + 'show_in_rest' => array( + 'type' => array( 'boolean', 'array' ), + 'schema' => array( + 'items' => array( + 'type' => 'string', + ), + ), + ), + ) + ); + register_theme_feature( + 'responsive-embeds', + array( + 'description' => __( 'Whether the theme supports responsive embedded content.' ), + 'show_in_rest' => true, + ) + ); + register_theme_feature( + 'title-tag', + array( + 'description' => __( 'Whether the theme can manage the document title tag.' ), + 'show_in_rest' => true, + ) + ); + register_theme_feature( + 'wp-block-styles', + array( + 'description' => __( 'Whether theme opts in to default WordPress block styles for viewing.' ), + 'show_in_rest' => true, + ) + ); +} diff --git a/tests/phpunit/tests/rest-api/rest-themes-controller.php b/tests/phpunit/tests/rest-api/rest-themes-controller.php index 1f7c53b374..0f39cfa7ab 100644 --- a/tests/phpunit/tests/rest-api/rest-themes-controller.php +++ b/tests/phpunit/tests/rest-api/rest-themes-controller.php @@ -260,7 +260,6 @@ class WP_Test_REST_Themes_Controller extends WP_Test_REST_Controller_Testcase { $this->assertArrayHasKey( 'version', $properties ); $theme_supports = $properties['theme_supports']['properties']; - $this->assertEquals( 20, count( $theme_supports ) ); $this->assertArrayHasKey( 'align-wide', $theme_supports ); $this->assertArrayHasKey( 'automatic-feed-links', $theme_supports ); $this->assertArrayHasKey( 'custom-header', $theme_supports ); @@ -281,6 +280,7 @@ class WP_Test_REST_Themes_Controller extends WP_Test_REST_Controller_Testcase { $this->assertArrayHasKey( 'responsive-embeds', $theme_supports ); $this->assertArrayHasKey( 'title-tag', $theme_supports ); $this->assertArrayHasKey( 'wp-block-styles', $theme_supports ); + $this->assertCount( 20, $theme_supports ); } /** @@ -997,6 +997,32 @@ class WP_Test_REST_Themes_Controller extends WP_Test_REST_Controller_Testcase { $this->assertEquals( array( 'post' ), $result[0]['theme_supports']['post-thumbnails'] ); } + /** + * @ticket 49406 + */ + public function test_variadic_theme_support() { + register_theme_feature( + 'test-feature', + array( + 'type' => 'array', + 'variadic' => true, + 'show_in_rest' => array( + 'schema' => array( + 'items' => array( + 'type' => 'string', + ), + ), + ), + ) + ); + add_theme_support( 'test-feature', 'a', 'b', 'c' ); + + $response = self::perform_active_theme_request(); + $result = $response->get_data(); + $this->assertTrue( isset( $result[0]['theme_supports'] ) ); + $this->assertEquals( array( 'a', 'b', 'c' ), $result[0]['theme_supports']['test-feature'] ); + } + /** * It should be possible to register custom fields to the endpoint. * diff --git a/tests/phpunit/tests/theme.php b/tests/phpunit/tests/theme.php index 5cd6f968a4..066931c15e 100644 --- a/tests/phpunit/tests/theme.php +++ b/tests/phpunit/tests/theme.php @@ -415,4 +415,288 @@ class Tests_Theme extends WP_UnitTestCase { $this->assertEquals( 'trash', get_post_status( $nav_created_post_ids[0] ) ); $this->assertEquals( 'private', get_post_status( $nav_created_post_ids[1] ) ); } + + /** + * @ticket 49406 + */ + public function test_register_theme_support_defaults() { + $registered = register_theme_feature( 'test-feature' ); + $this->assertTrue( $registered ); + + $expected = array( + 'type' => 'boolean', + 'variadic' => false, + 'description' => '', + 'show_in_rest' => false, + ); + $this->assertEqualSets( $expected, get_registered_theme_feature( 'test-feature' ) ); + } + + /** + * @ticket 49406 + */ + public function test_register_theme_support_explicit() { + $args = array( + 'type' => 'array', + 'variadic' => true, + 'description' => 'My Feature', + 'show_in_rest' => array( + 'schema' => array( + 'items' => array( + 'type' => 'string', + ), + ), + ), + ); + + register_theme_feature( 'test-feature', $args ); + $actual = get_registered_theme_feature( 'test-feature' ); + + $this->assertEquals( 'array', $actual['type'] ); + $this->assertTrue( $actual['variadic'] ); + $this->assertEquals( 'My Feature', $actual['description'] ); + $this->assertEquals( array( 'type' => 'string' ), $actual['show_in_rest']['schema']['items'] ); + } + + /** + * @ticket 49406 + */ + public function test_register_theme_support_upgrades_show_in_rest() { + register_theme_feature( 'test-feature', array( 'show_in_rest' => true ) ); + + $expected = array( + 'schema' => array( + 'type' => 'boolean', + 'description' => '', + 'default' => false, + ), + 'name' => 'test-feature', + 'prepare_callback' => null, + ); + $actual = get_registered_theme_feature( 'test-feature' )['show_in_rest']; + + $this->assertEqualSets( $expected, $actual ); + } + + /** + * @ticket 49406 + */ + public function test_register_theme_support_fills_schema() { + register_theme_feature( + 'test-feature', + array( + 'type' => 'array', + 'description' => 'Cool Feature', + 'show_in_rest' => array( + 'schema' => array( + 'items' => array( + 'type' => 'string', + ), + 'minItems' => 1, + ), + ), + ) + ); + + $expected = array( + 'description' => 'Cool Feature', + 'type' => array( 'boolean', 'array' ), + 'items' => array( + 'type' => 'string', + ), + 'minItems' => 1, + 'default' => false, + ); + $actual = get_registered_theme_feature( 'test-feature' )['show_in_rest']['schema']; + + $this->assertEqualSets( $expected, $actual ); + } + + /** + * @ticket 49406 + */ + public function test_register_theme_support_does_not_add_boolean_type_if_non_bool_default() { + register_theme_feature( + 'test-feature', + array( + 'type' => 'array', + 'show_in_rest' => array( + 'schema' => array( + 'items' => array( + 'type' => 'string', + ), + 'default' => array( 'standard' ), + ), + ), + ) + ); + + $actual = get_registered_theme_feature( 'test-feature' )['show_in_rest']['schema']['type']; + $this->assertEquals( 'array', $actual ); + } + + /** + * @ticket 49406 + */ + public function test_register_theme_support_defaults_additional_properties_to_false() { + register_theme_feature( + 'test-feature', + array( + 'type' => 'object', + 'description' => 'Cool Feature', + 'show_in_rest' => array( + 'schema' => array( + 'properties' => array( + 'a' => array( + 'type' => 'string', + ), + ), + ), + ), + ) + ); + + $actual = get_registered_theme_feature( 'test-feature' )['show_in_rest']['schema']; + + $this->assertArrayHasKey( 'additionalProperties', $actual ); + $this->assertFalse( $actual['additionalProperties'] ); + } + + /** + * @ticket 49406 + */ + public function test_register_theme_support_with_additional_properties() { + register_theme_feature( + 'test-feature', + array( + 'type' => 'object', + 'description' => 'Cool Feature', + 'show_in_rest' => array( + 'schema' => array( + 'properties' => array(), + 'additionalProperties' => array( + 'type' => 'string', + ), + ), + ), + ) + ); + + $expected = array( + 'type' => 'string', + ); + $actual = get_registered_theme_feature( 'test-feature' )['show_in_rest']['schema']['additionalProperties']; + + $this->assertEqualSets( $expected, $actual ); + } + + /** + * @ticket 49406 + */ + public function test_register_theme_support_defaults_additional_properties_to_false_in_array() { + register_theme_feature( + 'test-feature', + array( + 'type' => 'array', + 'description' => 'Cool Feature', + 'show_in_rest' => array( + 'schema' => array( + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'a' => array( + 'type' => 'string', + ), + ), + ), + ), + ), + ) + ); + + $actual = get_registered_theme_feature( 'test-feature' )['show_in_rest']['schema']['items']; + + $this->assertArrayHasKey( 'additionalProperties', $actual ); + $this->assertFalse( $actual['additionalProperties'] ); + } + + /** + * @ticket 49406 + * @dataProvider _dp_register_theme_support_validation + * @param string $error_code The error code expected. + * @param array $args The args to register. + */ + public function test_register_theme_support_validation( $error_code, $args ) { + $registered = register_theme_feature( 'test-feature', $args ); + + $this->assertWPError( $registered ); + $this->assertEquals( $error_code, $registered->get_error_code() ); + } + + public function _dp_register_theme_support_validation() { + return array( + array( + 'invalid_type', + array( + 'type' => 'float', + ), + ), + array( + 'invalid_type', + array( + 'type' => array( 'string' ), + ), + ), + array( + 'variadic_must_be_array', + array( + 'variadic' => true, + ), + ), + array( + 'missing_schema', + array( + 'type' => 'object', + 'show_in_rest' => true, + ), + ), + array( + 'missing_schema', + array( + 'type' => 'array', + 'show_in_rest' => true, + ), + ), + array( + 'missing_schema_items', + array( + 'type' => 'array', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'array', + ), + ), + ), + ), + array( + 'missing_schema_properties', + array( + 'type' => 'object', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + ), + ), + ), + ), + array( + 'invalid_rest_prepare_callback', + array( + 'show_in_rest' => array( + 'prepare_callback' => 'this is not a valid function', + ), + ), + ), + ); + } }