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
This commit is contained in:
Timothy Jacobs 2020-06-25 22:11:09 +00:00
parent 9568134d7b
commit 9a604011ee
6 changed files with 929 additions and 295 deletions

View File

@ -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( 'admin_enqueue_scripts', '_wp_customize_loader_settings' );
add_action( 'delete_attachment', '_delete_attachment_theme_mod' ); 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( '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. // Calendar widget cache.
add_action( 'save_post', 'delete_get_calendar_cache' ); add_action( 'save_post', 'delete_get_calendar_cache' );

View File

@ -1779,3 +1779,35 @@ function rest_filter_response_by_context( $data, $schema, $context ) {
return $data; 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;
}

View File

@ -167,50 +167,38 @@ class WP_REST_Themes_Controller extends WP_REST_Controller {
} }
if ( rest_is_field_included( 'theme_supports', $fields ) ) { if ( rest_is_field_included( 'theme_supports', $fields ) ) {
$item_schemas = $this->get_item_schema(); foreach ( get_registered_theme_features() as $feature => $config ) {
$theme_supports = $item_schemas['properties']['theme_supports']['properties']; if ( ! is_array( $config['show_in_rest'] ) ) {
foreach ( $theme_supports as $name => $schema ) { continue;
}
$name = $config['show_in_rest']['name'];
if ( ! rest_is_field_included( "theme_supports.{$name}", $fields ) ) { if ( ! rest_is_field_included( "theme_supports.{$name}", $fields ) ) {
continue; continue;
} }
if ( 'formats' === $name ) { if ( ! current_theme_supports( $feature ) ) {
$data['theme_supports'][ $name ] = $config['show_in_rest']['schema']['default'];
continue; continue;
} }
if ( ! current_theme_supports( $name ) ) { $support = get_theme_support( $feature );
$data['theme_supports'][ $name ] = false;
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; continue;
} }
if ( 'boolean' === $schema['type'] ) { $data['theme_supports'][ $name ] = $prepared;
$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 );
} }
$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 ); $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 ); 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. * 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.' ), 'description' => __( 'Features supported by this theme.' ),
'type' => 'object', 'type' => 'object',
'readonly' => true, 'readonly' => true,
'properties' => array( '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',
),
),
), ),
'theme_uri' => array( 'theme_uri' => array(
'description' => __( 'The URI of the theme\'s webpage.' ), '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; $this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema ); return $this->add_additional_fields_schema( $this->schema );

View File

@ -2993,6 +2993,156 @@ function require_if_theme_supports( $feature, $include ) {
return false; 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. * 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 ); 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,
)
);
}

View File

@ -260,7 +260,6 @@ class WP_Test_REST_Themes_Controller extends WP_Test_REST_Controller_Testcase {
$this->assertArrayHasKey( 'version', $properties ); $this->assertArrayHasKey( 'version', $properties );
$theme_supports = $properties['theme_supports']['properties']; $theme_supports = $properties['theme_supports']['properties'];
$this->assertEquals( 20, count( $theme_supports ) );
$this->assertArrayHasKey( 'align-wide', $theme_supports ); $this->assertArrayHasKey( 'align-wide', $theme_supports );
$this->assertArrayHasKey( 'automatic-feed-links', $theme_supports ); $this->assertArrayHasKey( 'automatic-feed-links', $theme_supports );
$this->assertArrayHasKey( 'custom-header', $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( 'responsive-embeds', $theme_supports );
$this->assertArrayHasKey( 'title-tag', $theme_supports ); $this->assertArrayHasKey( 'title-tag', $theme_supports );
$this->assertArrayHasKey( 'wp-block-styles', $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'] ); $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. * It should be possible to register custom fields to the endpoint.
* *

View File

@ -415,4 +415,288 @@ class Tests_Theme extends WP_UnitTestCase {
$this->assertEquals( 'trash', get_post_status( $nav_created_post_ids[0] ) ); $this->assertEquals( 'trash', get_post_status( $nav_created_post_ids[0] ) );
$this->assertEquals( 'private', get_post_status( $nav_created_post_ids[1] ) ); $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',
),
),
),
);
}
} }