REST API: Add widget endpoints

Adds the sidebars, widgets and widget-types REST API endpoints from the
Gutenberg plugin.

Fixes #41683.
Props TimothyBlynJacobs.

Built from https://develop.svn.wordpress.org/trunk@50993


git-svn-id: http://core.svn.wordpress.org/trunk@50602 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
noisysocks 2021-05-25 07:52:55 +00:00
parent 2b7ad8ad4f
commit 2a4e1e0c04
24 changed files with 2151 additions and 5 deletions

View File

@ -102,4 +102,22 @@ class WP_Widget_Factory {
$this->widgets[ $key ]->_register(); $this->widgets[ $key ]->_register();
} }
} }
/**
* Returns the registered WP_Widget object for the given widget type.
*
* @since 5.8.0
*
* @param string $id_base Widget type ID.
* @return WP_Widget|null
*/
public function get_widget_object( $id_base ) {
foreach ( $this->widgets as $widget_object ) {
if ( $widget_object->id_base === $id_base ) {
return $widget_object;
}
}
return null;
}
} }

View File

@ -63,3 +63,6 @@ require_once ABSPATH . WPINC . '/widgets/class-wp-nav-menu-widget.php';
/** WP_Widget_Custom_HTML class */ /** WP_Widget_Custom_HTML class */
require_once ABSPATH . WPINC . '/widgets/class-wp-widget-custom-html.php'; require_once ABSPATH . WPINC . '/widgets/class-wp-widget-custom-html.php';
/** WP_Widget_Block class */
require_once ABSPATH . WPINC . '/widgets/class-wp-widget-block.php';

View File

@ -313,6 +313,18 @@ function create_initial_rest_routes() {
$controller = new WP_REST_Plugins_Controller(); $controller = new WP_REST_Plugins_Controller();
$controller->register_routes(); $controller->register_routes();
// Sidebars.
$controller = new WP_REST_Sidebars_Controller();
$controller->register_routes();
// Widget Types.
$controller = new WP_REST_Widget_Types_Controller();
$controller->register_routes();
// Widgets.
$controller = new WP_REST_Widgets_Controller();
$controller->register_routes();
// Block Directory. // Block Directory.
$controller = new WP_REST_Block_Directory_Controller(); $controller = new WP_REST_Block_Directory_Controller();
$controller->register_routes(); $controller->register_routes();

View File

@ -0,0 +1,459 @@
<?php
/**
* REST API: WP_REST_Sidebars_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 5.8.0
*
* Copyright (C) 2015 Martin Pettersson
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Martin Pettersson <martin_pettersson@outlook.com>
* @copyright 2015 Martin Pettersson
* @license GPLv2
* @link https://github.com/martin-pettersson/wp-rest-api-sidebars
*/
/**
* Core class used to manage a site's sidebars.
*
* @since 5.8.0
*
* @see WP_REST_Controller
*/
class WP_REST_Sidebars_Controller extends WP_REST_Controller {
/**
* Sidebars controller constructor.
*
* @since 5.8.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'sidebars';
}
/**
* Registers the controllers routes.
*
* @since 5.8.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' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\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 registered sidebar' ),
'type' => 'string',
),
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
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 ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks if a given request has access to get sidebars.
*
* @since 5.8.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 ) {
return $this->do_permissions_check();
}
/**
* Retrieves the list of sidebars (active or inactive).
*
* @since 5.8.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 ) {
$data = array();
foreach ( (array) wp_get_sidebars_widgets() as $id => $widgets ) {
$sidebar = $this->get_sidebar( $id );
if ( ! $sidebar ) {
continue;
}
$data[] = $this->prepare_response_for_collection(
$this->prepare_item_for_response( $sidebar, $request )
);
}
return rest_ensure_response( $data );
}
/**
* Checks if a given request has access to get a single sidebar.
*
* @since 5.8.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_item_permissions_check( $request ) {
return $this->do_permissions_check();
}
/**
* Retrieves one sidebar from the collection.
*
* @since 5.8.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 ) {
$sidebar = $this->get_sidebar( $request['id'] );
if ( ! $sidebar ) {
return new WP_Error( 'rest_sidebar_not_found', __( 'No sidebar exists with that id.' ), array( 'status' => 404 ) );
}
return $this->prepare_item_for_response( $sidebar, $request );
}
/**
* Checks if a given request has access to update sidebars.
*
* @since 5.8.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 update_item_permissions_check( $request ) {
return $this->do_permissions_check();
}
/**
* Updates a sidebar.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response Response object on success, or WP_Error object on failure.
*/
public function update_item( $request ) {
if ( isset( $request['widgets'] ) ) {
$sidebars = wp_get_sidebars_widgets();
foreach ( $sidebars as $sidebar_id => $widgets ) {
foreach ( $widgets as $i => $widget_id ) {
// This automatically removes the passed widget ids from any other sidebars in use.
if ( $sidebar_id !== $request['id'] && in_array( $widget_id, $request['widgets'], true ) ) {
unset( $sidebars[ $sidebar_id ][ $i ] );
}
// This automatically removes omitted widget ids to the inactive sidebar.
if ( $sidebar_id === $request['id'] && ! in_array( $widget_id, $request['widgets'], true ) ) {
$sidebars['wp_inactive_widgets'][] = $widget_id;
}
}
}
$sidebars[ $request['id'] ] = $request['widgets'];
wp_set_sidebars_widgets( $sidebars );
}
$request['context'] = 'edit';
$sidebar = $this->get_sidebar( $request['id'] );
return $this->prepare_item_for_response( $sidebar, $request );
}
/**
* Checks if the user has permissions to make the request.
*
* @since 5.8.0
*
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
protected function do_permissions_check() {
// Verify if the current user has edit_theme_options capability.
// This capability is required to access the widgets screen.
if ( ! current_user_can( 'edit_theme_options' ) ) {
return new WP_Error(
'rest_cannot_manage_widgets',
__( 'Sorry, you are not allowed to manage widgets on this site.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Retrieves the registered sidebar with the given id.
*
* @since 5.8.0
*
* @global array $wp_registered_sidebars The registered sidebars.
*
* @param string|int $id ID of the sidebar.
* @return array|null The discovered sidebar, or null if it is not registered.
*/
protected function get_sidebar( $id ) {
global $wp_registered_sidebars;
foreach ( (array) $wp_registered_sidebars as $sidebar ) {
if ( $sidebar['id'] === $id ) {
return $sidebar;
}
}
if ( 'wp_inactive_widgets' === $id ) {
return array(
'id' => 'wp_inactive_widgets',
'name' => __( 'Inactive widgets' ),
);
}
return null;
}
/**
* Prepares a single sidebar output for response.
*
* @since 5.8.0
*
* @global array $wp_registered_sidebars The registered sidebars.
* @global array $wp_registered_widgets The registered widgets.
*
* @param array $raw_sidebar Sidebar instance.
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_REST_Response Prepared response object.
*/
public function prepare_item_for_response( $raw_sidebar, $request ) {
global $wp_registered_sidebars, $wp_registered_widgets;
$id = $raw_sidebar['id'];
$sidebar = array( 'id' => $id );
if ( isset( $wp_registered_sidebars[ $id ] ) ) {
$registered_sidebar = $wp_registered_sidebars[ $id ];
$sidebar['status'] = 'active';
$sidebar['name'] = isset( $registered_sidebar['name'] ) ? $registered_sidebar['name'] : '';
$sidebar['description'] = isset( $registered_sidebar['description'] ) ? $registered_sidebar['description'] : '';
$sidebar['class'] = isset( $registered_sidebar['class'] ) ? $registered_sidebar['class'] : '';
$sidebar['before_widget'] = isset( $registered_sidebar['before_widget'] ) ? $registered_sidebar['before_widget'] : '';
$sidebar['after_widget'] = isset( $registered_sidebar['after_widget'] ) ? $registered_sidebar['after_widget'] : '';
$sidebar['before_title'] = isset( $registered_sidebar['before_title'] ) ? $registered_sidebar['before_title'] : '';
$sidebar['after_title'] = isset( $registered_sidebar['after_title'] ) ? $registered_sidebar['after_title'] : '';
} else {
$sidebar['status'] = 'inactive';
$sidebar['name'] = $raw_sidebar['name'];
$sidebar['description'] = '';
$sidebar['class'] = '';
}
$fields = $this->get_fields_for_response( $request );
if ( rest_is_field_included( 'widgets', $fields ) ) {
$sidebars = wp_get_sidebars_widgets();
$widgets = array_filter(
isset( $sidebars[ $sidebar['id'] ] ) ? $sidebars[ $sidebar['id'] ] : array(),
static function ( $widget_id ) use ( $wp_registered_widgets ) {
return isset( $wp_registered_widgets[ $widget_id ] );
}
);
$sidebar['widgets'] = $widgets;
}
$schema = $this->get_item_schema();
$data = array();
foreach ( $schema['properties'] as $property_id => $property ) {
if ( isset( $sidebar[ $property_id ] ) && true === rest_validate_value_from_schema( $sidebar[ $property_id ], $property ) ) {
$data[ $property_id ] = $sidebar[ $property_id ];
} elseif ( isset( $property['default'] ) ) {
$data[ $property_id ] = $property['default'];
}
}
$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( $sidebar ) );
/**
* Filters the REST API response for a sidebar.
*
* @since 5.8.0
*
* @param WP_REST_Response $response The response object.
* @param array $raw_sidebar The raw sidebar data.
* @param WP_REST_Request $request The request object.
*/
return apply_filters( 'rest_prepare_sidebar', $response, $raw_sidebar, $request );
}
/**
* Prepares links for the sidebar.
*
* @since 5.8.0
*
* @param array $sidebar Sidebar.
*
* @return array Links for the given widget.
*/
protected function prepare_links( $sidebar ) {
return 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, $sidebar['id'] ) ),
),
'https://api.w.org/widget' => array(
'href' => add_query_arg( 'sidebar', $sidebar['id'], rest_url( '/wp/v2/widgets' ) ),
'embeddable' => true,
),
);
}
/**
* 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' => 'sidebar',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'ID of sidebar.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Unique name identifying the sidebar.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'Description of sidebar.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'class' => array(
'description' => __( 'Extra CSS class to assign to the sidebar in the Widgets interface.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'before_widget' => array(
'description' => __( 'HTML content to prepend to each widget\'s HTML output when assigned to this sidebar. Default is an opening list item element.' ),
'type' => 'string',
'default' => '',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'after_widget' => array(
'description' => __( 'HTML content to append to each widget\'s HTML output when assigned to this sidebar. Default is a closing list item element.' ),
'type' => 'string',
'default' => '',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'before_title' => array(
'description' => __( 'HTML content to prepend to the sidebar title when displayed. Default is an opening h2 element.' ),
'type' => 'string',
'default' => '',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'after_title' => array(
'description' => __( 'HTML content to append to the sidebar title when displayed. Default is a closing h2 element.' ),
'type' => 'string',
'default' => '',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'status' => array(
'description' => __( 'Status of sidebar.' ),
'type' => 'string',
'enum' => array( 'active', 'inactive' ),
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'widgets' => array(
'description' => __( 'Nested widgets.' ),
'type' => 'array',
'items' => array(
'type' => array( 'object', 'string' ),
),
'default' => array(),
'context' => array( 'embed', 'view', 'edit' ),
),
),
);
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
}

View File

@ -0,0 +1,551 @@
<?php
/**
* REST API: WP_REST_Widget_Types_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 5.8.0
*/
/**
* Core class to access widget types via the REST API.
*
* @since 5.8.0
*
* @see WP_REST_Controller
*/
class WP_REST_Widget_Types_Controller extends WP_REST_Controller {
/**
* Constructor.
*
* @since 5.8.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'widget-types';
}
/**
* Registers the widget type routes.
*
* @since 5.8.0
*
* @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<id>[a-zA-Z0-9_-]+)',
array(
'args' => array(
'id' => array(
'description' => __( 'The widget type id.' ),
'type' => 'string',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[a-zA-Z0-9_-]+)/encode',
array(
'args' => array(
'id' => array(
'description' => __( 'The widget type id.' ),
'type' => 'string',
'required' => true,
),
'instance' => array(
'description' => __( 'Current instance settings of the widget.' ),
'type' => 'object',
),
'form_data' => array(
'description' => __( 'Serialized widget form data to encode into instance settings.' ),
'type' => 'string',
'sanitize_callback' => function( $string ) {
$array = array();
wp_parse_str( $string, $array );
return $array;
},
),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'callback' => array( $this, 'encode_form_data' ),
),
)
);
}
/**
* Checks whether a given request has permission to read widget types.
*
* @since 5.8.0
*
* @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 ) {
return $this->check_read_permission();
}
/**
* Retrieves the list of all widget types.
*
* @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 ) {
$data = array();
foreach ( $this->get_widgets() as $widget ) {
$widget_type = $this->prepare_item_for_response( $widget, $request );
$data[] = $this->prepare_response_for_collection( $widget_type );
}
return rest_ensure_response( $data );
}
/**
* Checks if a given request has access to read a widget type.
*
* @since 5.8.0
*
* @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;
}
$widget_id = $request['id'];
$widget_type = $this->get_widget( $widget_id );
if ( is_wp_error( $widget_type ) ) {
return $widget_type;
}
return true;
}
/**
* Checks whether the user can read widget types.
*
* @since 5.8.0
*
* @return WP_Error|bool True if the widget type is visible, WP_Error otherwise.
*/
protected function check_read_permission() {
if ( ! current_user_can( 'edit_theme_options' ) ) {
return new WP_Error(
'rest_cannot_manage_widgets',
__( 'Sorry, you are not allowed to manage widgets on this site.' ),
array(
'status' => rest_authorization_required_code(),
)
);
}
return true;
}
/**
* Gets the details about the requested widget.
*
* @since 5.8.0
*
* @param string $id The widget type id.
* @return array|WP_Error The array of widget data if the name is valid, WP_Error otherwise.
*/
public function get_widget( $id ) {
foreach ( $this->get_widgets() as $widget ) {
if ( $id === $widget['id'] ) {
return $widget;
}
}
return new WP_Error( 'rest_widget_type_invalid', __( 'Invalid widget type.' ), array( 'status' => 404 ) );
}
/**
* Normalize array of widgets.
*
* @since 5.8.0
*
* @global array $wp_registered_widgets The list of registered widgets.
*
* @return array Array of widgets.
*/
protected function get_widgets() {
global $wp_widget_factory, $wp_registered_widgets;
$widgets = array();
foreach ( $wp_registered_widgets as $widget ) {
$parsed_id = wp_parse_widget_id( $widget['id'] );
$widget_object = $wp_widget_factory->get_widget_object( $parsed_id['id_base'] );
$widget['id'] = $parsed_id['id_base'];
$widget['is_multi'] = (bool) $widget_object;
unset( $widget['callback'] );
$classname = '';
foreach ( (array) $widget['classname'] as $cn ) {
if ( is_string( $cn ) ) {
$classname .= '_' . $cn;
} elseif ( is_object( $cn ) ) {
$classname .= '_' . get_class( $cn );
}
}
$widget['classname'] = ltrim( $classname, '_' );
$widgets[] = $widget;
}
return $widgets;
}
/**
* Retrieves a single widget type from the collection.
*
* @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_item( $request ) {
$widget_id = $request['id'];
$widget_type = $this->get_widget( $widget_id );
if ( is_wp_error( $widget_type ) ) {
return $widget_type;
}
$data = $this->prepare_item_for_response( $widget_type, $request );
return rest_ensure_response( $data );
}
/**
* Prepares a widget type object for serialization.
*
* @since 5.8.0
*
* @param array $widget_type Widget type data.
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response Widget type data.
*/
public function prepare_item_for_response( $widget_type, $request ) {
$fields = $this->get_fields_for_response( $request );
$data = array(
'id' => $widget_type['id'],
);
$schema = $this->get_item_schema();
$extra_fields = array(
'name',
'description',
'is_multi',
'classname',
'widget_class',
'option_name',
'customize_selective_refresh',
);
foreach ( $extra_fields as $extra_field ) {
if ( ! rest_is_field_included( $extra_field, $fields ) ) {
continue;
}
if ( isset( $widget_type[ $extra_field ] ) ) {
$field = $widget_type[ $extra_field ];
} elseif ( array_key_exists( 'default', $schema['properties'][ $extra_field ] ) ) {
$field = $schema['properties'][ $extra_field ]['default'];
} else {
$field = '';
}
$data[ $extra_field ] = rest_sanitize_value_from_schema( $field, $schema['properties'][ $extra_field ] );
}
$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( $widget_type ) );
/**
* Filters the REST API response for a widget type.
*
* @since 5.8.0
*
* @param WP_REST_Response $response The response object.
* @param array $widget_type The array of widget data.
* @param WP_REST_Request $request The request object.
*/
return apply_filters( 'rest_prepare_widget_type', $response, $widget_type, $request );
}
/**
* Prepares links for the widget type.
*
* @since 5.8.0
*
* @param array $widget_type Widget type data.
* @return array Links for the given widget type.
*/
protected function prepare_links( $widget_type ) {
return 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, $widget_type['id'] ) ),
),
);
}
/**
* Retrieves the widget type'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 );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'widget-type',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique slug identifying the widget type.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Human-readable name identifying the widget type.' ),
'type' => 'string',
'default' => '',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'Description of the widget.' ),
'type' => 'string',
'default' => '',
'context' => array( 'view', 'edit', 'embed' ),
),
'is_multi' => array(
'description' => __( 'Whether the widget supports multiple instances' ),
'type' => 'boolean',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'classname' => array(
'description' => __( 'Class name' ),
'type' => 'string',
'default' => '',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
),
);
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
/**
* An RPC-style endpoint which can be used by clients to turn user input in
* a widget admin form into an encoded instance object.
*
* Accepts:
*
* - id: A widget type ID.
* - instance: A widget's encoded instance object. Optional.
* - form_data: Form data from submitting a widget's admin form. Optional.
*
* Returns:
* - instance: The encoded instance object after updating the widget with
* the given form data.
* - form: The widget's admin form after updating the widget with the
* given form data.
*
* @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 encode_form_data( $request ) {
global $wp_widget_factory;
$id = $request['id'];
$widget_object = $wp_widget_factory->get_widget_object( $id );
if ( ! $widget_object ) {
return new WP_Error(
'rest_invalid_widget',
__( 'Cannot preview a widget that does not extend WP_Widget.' ),
array( 'status' => 400 )
);
}
// Set the widget's number so that the id attributes in the HTML that we
// return are predictable.
if ( isset( $request['number'] ) && is_numeric( $request['number'] ) ) {
$widget_object->_set( (int) $request['number'] );
} else {
$widget_object->_set( -1 );
}
if ( isset( $request['instance']['encoded'], $request['instance']['hash'] ) ) {
$serialized_instance = base64_decode( $request['instance']['encoded'] );
if ( ! hash_equals( wp_hash( $serialized_instance ), $request['instance']['hash'] ) ) {
return new WP_Error(
'rest_invalid_widget',
__( 'The provided instance is malformed.' ),
array( 'status' => 400 )
);
}
$instance = unserialize( $serialized_instance );
} else {
$instance = array();
}
if (
isset( $request['form_data'][ "widget-$id" ] ) &&
is_array( $request['form_data'][ "widget-$id" ] )
) {
$new_instance = array_values( $request['form_data'][ "widget-$id" ] )[0];
$old_instance = $instance;
$instance = $widget_object->update( $new_instance, $old_instance );
/** This filter is documented in wp-includes/class-wp-widget.php */
$instance = apply_filters(
'widget_update_callback',
$instance,
$new_instance,
$old_instance,
$widget_object
);
}
$serialized_instance = serialize( $instance );
$response = array(
'form' => trim(
$this->get_widget_form(
$widget_object,
$instance
)
),
'preview' => trim(
$this->get_widget_preview(
$widget_object,
$instance
)
),
'instance' => array(
'encoded' => base64_encode( $serialized_instance ),
'hash' => wp_hash( $serialized_instance ),
),
);
if ( ! empty( $widget_object->widget_options['show_instance_in_rest'] ) ) {
// Use new stdClass so that JSON result is {} and not [].
$response['instance']['raw'] = empty( $instance ) ? new stdClass : $instance;
}
return rest_ensure_response( $response );
}
/**
* Returns the output of WP_Widget::widget() when called with the provided
* instance. Used by encode_form_data() to preview a widget.
* @param WP_Widget $widget_object Widget object to call widget() on.
* @param array $instance Widget instance settings.
* @return string
*/
private function get_widget_preview( $widget_object, $instance ) {
ob_start();
the_widget( get_class( $widget_object ), $instance );
return ob_get_clean();
}
/**
* Returns the output of WP_Widget::form() when called with the provided
* instance. Used by encode_form_data() to preview a widget's form.
*
* @param WP_Widget $widget_object Widget object to call widget() on.
* @param array $instance Widget instance settings.
* @return string
*/
private function get_widget_form( $widget_object, $instance ) {
ob_start();
/** This filter is documented in wp-includes/class-wp-widget.php */
$instance = apply_filters(
'widget_form_callback',
$instance,
$widget_object
);
if ( false !== $instance ) {
$return = $widget_object->form( $instance );
/** This filter is documented in wp-includes/class-wp-widget.php */
do_action_ref_array(
'in_widget_form',
array( &$widget_object, &$return, $instance )
);
}
return ob_get_clean();
}
/**
* Retrieves the query params for collections.
*
* @since 5.8.0
*
* @return array Collection parameters.
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
}
}

View File

@ -0,0 +1,693 @@
<?php
/**
* REST API: WP_REST_Widgets_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 5.8.0
*/
/**
* Core class to access widgets via the REST API.
*
* @since 5.8.0
*
* @see WP_REST_Controller
*/
class WP_REST_Widgets_Controller extends WP_REST_Controller {
/**
* Widgets controller constructor.
*
* @since 5.8.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'widgets';
}
/**
* Registers the widget routes for the controller.
*
* @since 5.8.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' => $this->get_endpoint_args_for_item_schema(),
),
'allow_batch' => array( 'v1' => true ),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
$this->rest_base . '/(?P<id>[\w\-]+)',
array(
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' ) ),
),
),
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(
'description' => __( 'Whether to force removal of the widget, or move it to the inactive sidebar.' ),
'type' => 'boolean',
),
),
),
'allow_batch' => array( 'v1' => true ),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks if a given request has access to get widgets.
*
* @since 5.8.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 ) {
return $this->permissions_check();
}
/**
* Retrieves a collection of widgets.
*
* @since 5.8.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 ) {
$prepared = array();
foreach ( wp_get_sidebars_widgets() as $sidebar_id => $widget_ids ) {
if ( isset( $request['sidebar'] ) && $sidebar_id !== $request['sidebar'] ) {
continue;
}
foreach ( $widget_ids as $widget_id ) {
$response = $this->prepare_item_for_response( compact( 'sidebar_id', 'widget_id' ), $request );
if ( ! is_wp_error( $response ) ) {
$prepared[] = $this->prepare_response_for_collection( $response );
}
}
}
return new WP_REST_Response( $prepared );
}
/**
* Checks if a given request has access to get a widget.
*
* @since 5.8.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_item_permissions_check( $request ) {
return $this->permissions_check();
}
/**
* Gets an individual widget.
*
* @since 5.8.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 ) {
$widget_id = $request['id'];
$sidebar_id = wp_find_widgets_sidebar( $widget_id );
if ( is_null( $sidebar_id ) ) {
return new WP_Error(
'rest_widget_not_found',
__( 'No widget was found with that id.' ),
array( 'status' => 404 )
);
}
return $this->prepare_item_for_response( compact( 'widget_id', 'sidebar_id' ), $request );
}
/**
* Checks if a given request has access to create widgets.
*
* @since 5.8.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 create_item_permissions_check( $request ) {
return $this->permissions_check();
}
/**
* Creates a widget.
*
* @since 5.8.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 ) {
$sidebar_id = $request['sidebar'];
$widget_id = $this->save_widget( $request );
if ( is_wp_error( $widget_id ) ) {
return $widget_id;
}
wp_assign_widget_to_sidebar( $widget_id, $sidebar_id );
$request['context'] = 'edit';
$response = $this->prepare_item_for_response( compact( 'sidebar_id', 'widget_id' ), $request );
if ( is_wp_error( $response ) ) {
return $response;
}
$response->set_status( 201 );
return $response;
}
/**
* Checks if a given request has access to update widgets.
*
* @since 5.8.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 update_item_permissions_check( $request ) {
return $this->permissions_check();
}
/**
* Updates an existing widget.
*
* @since 5.8.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 ) {
global $wp_widget_factory;
$widget_id = $request['id'];
$sidebar_id = wp_find_widgets_sidebar( $widget_id );
// Allow sidebar to be unset or missing when widget is not a WP_Widget.
$parsed_id = wp_parse_widget_id( $widget_id );
$widget_object = $wp_widget_factory->get_widget_object( $parsed_id['id_base'] );
if ( is_null( $sidebar_id ) && $widget_object ) {
return new WP_Error(
'rest_widget_not_found',
__( 'No widget was found with that id.' ),
array( 'status' => 404 )
);
}
if (
$request->has_param( 'instance' ) ||
$request->has_param( 'form_data' )
) {
$maybe_error = $this->save_widget( $request );
if ( is_wp_error( $maybe_error ) ) {
return $maybe_error;
}
}
if ( $request->has_param( 'sidebar' ) ) {
if ( $sidebar_id !== $request['sidebar'] ) {
$sidebar_id = $request['sidebar'];
wp_assign_widget_to_sidebar( $widget_id, $sidebar_id );
}
}
$request['context'] = 'edit';
return $this->prepare_item_for_response( compact( 'widget_id', 'sidebar_id' ), $request );
}
/**
* Checks if a given request has access to delete widgets.
*
* @since 5.8.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 delete_item_permissions_check( $request ) {
return $this->permissions_check();
}
/**
* Deletes a widget.
*
* @since 5.8.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 ) {
$widget_id = $request['id'];
$sidebar_id = wp_find_widgets_sidebar( $widget_id );
if ( is_null( $sidebar_id ) ) {
return new WP_Error(
'rest_widget_not_found',
__( 'No widget was found with that id.' ),
array( 'status' => 404 )
);
}
$request['context'] = 'edit';
if ( $request['force'] ) {
$prepared = $this->prepare_item_for_response( compact( 'widget_id', 'sidebar_id' ), $request );
wp_assign_widget_to_sidebar( $widget_id, '' );
$prepared->set_data(
array(
'deleted' => true,
'previous' => $prepared->get_data(),
)
);
} else {
wp_assign_widget_to_sidebar( $widget_id, 'wp_inactive_widgets' );
$prepared = $this->prepare_item_for_response(
array(
'sidebar_id' => 'wp_inactive_widgets',
'widget_id' => $widget_id,
),
$request
);
}
return $prepared;
}
/**
* Performs a permissions check for managing widgets.
*
* @since 5.8.0
*
* @return true|WP_Error
*/
protected function permissions_check() {
if ( ! current_user_can( 'edit_theme_options' ) ) {
return new WP_Error(
'rest_cannot_manage_widgets',
__( 'Sorry, you are not allowed to manage widgets on this site.' ),
array(
'status' => rest_authorization_required_code(),
)
);
}
return true;
}
/**
* Saves the widget in the request object.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
*
* @return string|WP_Error The saved widget ID.
*/
protected function save_widget( $request ) {
global $wp_widget_factory, $wp_registered_widget_updates;
require_once ABSPATH . 'wp-admin/includes/widgets.php'; // For next_widget_id_number().
if ( isset( $request['id'] ) ) {
// Saving an existing widget.
$id = $request['id'];
$parsed_id = wp_parse_widget_id( $id );
$id_base = $parsed_id['id_base'];
$number = isset( $parsed_id['number'] ) ? $parsed_id['number'] : null;
$widget_object = $wp_widget_factory->get_widget_object( $id_base );
} elseif ( $request['id_base'] ) {
// Saving a new widget.
$id_base = $request['id_base'];
$widget_object = $wp_widget_factory->get_widget_object( $id_base );
$number = $widget_object ? next_widget_id_number( $id_base ) : null;
$id = $widget_object ? $id_base . '-' . $number : $id_base;
} else {
return new WP_Error(
'rest_invalid_widget',
__( 'Widget type (id_base) is required.' ),
array( 'status' => 400 )
);
}
if ( ! isset( $wp_registered_widget_updates[ $id_base ] ) ) {
return new WP_Error(
'rest_invalid_widget',
__( 'The provided widget type (id_base) cannot be updated.' ),
array( 'status' => 400 )
);
}
if ( isset( $request['instance'] ) ) {
if ( ! $widget_object ) {
return new WP_Error(
'rest_invalid_widget',
__( 'Cannot set instance on a widget that does not extend WP_Widget.' ),
array( 'status' => 400 )
);
}
if ( isset( $request['instance']['raw'] ) ) {
if ( empty( $widget_object->widget_options['show_instance_in_rest'] ) ) {
return new WP_Error(
'rest_invalid_widget',
__( 'Widget type does not support raw instances.' ),
array( 'status' => 400 )
);
}
$instance = $request['instance']['raw'];
} elseif ( isset( $request['instance']['encoded'], $request['instance']['hash'] ) ) {
$serialized_instance = base64_decode( $request['instance']['encoded'] );
if ( ! hash_equals( wp_hash( $serialized_instance ), $request['instance']['hash'] ) ) {
return new WP_Error(
'rest_invalid_widget',
__( 'The provided instance is malformed.' ),
array( 'status' => 400 )
);
}
$instance = unserialize( $serialized_instance );
} else {
return new WP_Error(
'rest_invalid_widget',
__( 'The provided instance is invalid. Must contain raw OR encoded and hash.' ),
array( 'status' => 400 )
);
}
$form_data = array(
"widget-$id_base" => array(
$number => $instance,
),
);
} elseif ( isset( $request['form_data'] ) ) {
$form_data = $request['form_data'];
} else {
$form_data = array();
}
$original_post = $_POST;
$original_request = $_REQUEST;
foreach ( $form_data as $key => $value ) {
$slashed_value = wp_slash( $value );
$_POST[ $key ] = $slashed_value;
$_REQUEST[ $key ] = $slashed_value;
}
$callback = $wp_registered_widget_updates[ $id_base ]['callback'];
$params = $wp_registered_widget_updates[ $id_base ]['params'];
if ( is_callable( $callback ) ) {
ob_start();
call_user_func_array( $callback, $params );
ob_end_clean();
}
$_POST = $original_post;
$_REQUEST = $original_request;
if ( $widget_object ) {
// Register any multi-widget that the update callback just created.
$widget_object->_set( $number );
$widget_object->_register_one( $number );
// WP_Widget sets updated = true after an update to prevent more
// than one widget from being saved per request. This isn't what we
// want in the REST API, though, as we support batch requests.
$widget_object->updated = false;
}
return $id;
}
/**
* Prepares the widget for the REST response.
*
* @since 5.8.0
*
* @global array $wp_registered_sidebars The registered sidebars.
* @global array $wp_registered_widgets The registered widgets.
* @global array $wp_registered_widget_controls The registered widget controls.
*
* @param array $item An array containing a widget_id and sidebar_id.
* @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 ) {
global $wp_widget_factory, $wp_registered_widgets;
$widget_id = $item['widget_id'];
$sidebar_id = $item['sidebar_id'];
if ( ! isset( $wp_registered_widgets[ $widget_id ] ) ) {
return new WP_Error(
'rest_invalid_widget',
__( 'The requested widget is invalid.' ),
array( 'status' => 500 )
);
}
$widget = $wp_registered_widgets[ $widget_id ];
$parsed_id = wp_parse_widget_id( $widget_id );
$fields = $this->get_fields_for_response( $request );
$prepared = array(
'id' => $widget_id,
'id_base' => $parsed_id['id_base'],
'sidebar' => $sidebar_id,
'rendered' => '',
'rendered_form' => null,
'instance' => null,
);
if (
rest_is_field_included( 'rendered', $fields ) &&
'wp_inactive_widgets' !== $sidebar_id
) {
$prepared['rendered'] = trim( wp_render_widget( $widget_id, $sidebar_id ) );
}
if ( rest_is_field_included( 'rendered_form', $fields ) ) {
$rendered_form = wp_render_widget_control( $widget_id );
if ( ! is_null( $rendered_form ) ) {
$prepared['rendered_form'] = trim( $rendered_form );
}
}
if ( rest_is_field_included( 'instance', $fields ) ) {
$widget_object = $wp_widget_factory->get_widget_object( $parsed_id['id_base'] );
if ( $widget_object && isset( $parsed_id['number'] ) ) {
$all_instances = $widget_object->get_settings();
$instance = $all_instances[ $parsed_id['number'] ];
$serialized_instance = serialize( $instance );
$prepared['instance']['encoded'] = base64_encode( $serialized_instance );
$prepared['instance']['hash'] = wp_hash( $serialized_instance );
if ( ! empty( $widget_object->widget_options['show_instance_in_rest'] ) ) {
// Use new stdClass so that JSON result is {} and not [].
$prepared['instance']['raw'] = empty( $instance ) ? new stdClass : $instance;
}
}
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$prepared = $this->add_additional_fields_to_object( $prepared, $request );
$prepared = $this->filter_response_by_context( $prepared, $context );
$response = rest_ensure_response( $prepared );
$response->add_links( $this->prepare_links( $prepared ) );
/**
* Filters the REST API response for a widget.
*
* @since 5.8.0
*
* @param WP_REST_Response $response The response object.
* @param array $widget The registered widget data.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'rest_prepare_widget', $response, $widget, $request );
}
/**
* Prepares links for the widget.
*
* @since 5.8.0
*
* @param array $prepared Widget.
* @return array Links for the given widget.
*/
protected function prepare_links( $prepared ) {
$id_base = ! empty( $prepared['id_base'] ) ? $prepared['id_base'] : $prepared['id'];
return array(
'self' => array(
'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $prepared['id'] ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
),
'about' => array(
'href' => rest_url( sprintf( 'wp/v2/widget-types/%s', $id_base ) ),
'embeddable' => true,
),
'https://api.w.org/sidebar' => array(
'href' => rest_url( sprintf( 'wp/v2/sidebars/%s/', $prepared['sidebar'] ) ),
),
);
}
/**
* Gets the list of collection params.
*
* @since 5.8.0
*
* @return array[]
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
'sidebar' => array(
'description' => __( 'The sidebar to return widgets for.' ),
'type' => 'string',
),
);
}
/**
* Retrieves the widget'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' => 'widget',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the widget.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
),
'id_base' => array(
'description' => __( 'The type of the widget. Corresponds to ID in widget-types endpoint.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
),
'sidebar' => array(
'description' => __( 'The sidebar the widget belongs to.' ),
'type' => 'string',
'default' => 'wp_inactive_widgets',
'required' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'rendered' => array(
'description' => __( 'HTML representation of the widget.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'rendered_form' => array(
'description' => __( 'HTML representation of the widget admin form.' ),
'type' => 'string',
'context' => array( 'edit' ),
'readonly' => true,
),
'instance' => array(
'description' => __( 'Instance settings of the widget, if supported.' ),
'type' => 'object',
'context' => array( 'view', 'edit', 'embed' ),
'default' => null,
'properties' => array(
'encoded' => array(
'description' => __( 'Base64 encoded representation of the instance settings.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
),
'hash' => array(
'description' => __( 'Cryptographic hash of the instance settings.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
),
'raw' => array(
'description' => __( 'Unencoded instance settings, if supported.' ),
'type' => 'object',
'context' => array( 'view', 'edit', 'embed' ),
),
),
),
'form_data' => array(
'description' => __( 'URL-encoded form data from the widget admin form. Used to update a widget that does not support instance. Write only.' ),
'type' => 'string',
'context' => array(),
'arg_options' => array(
'sanitize_callback' => function( $string ) {
$array = array();
wp_parse_str( $string, $array );
return $array;
},
),
),
),
);
return $this->add_additional_fields_schema( $this->schema );
}
}

View File

@ -13,7 +13,7 @@
* *
* @global string $wp_version * @global string $wp_version
*/ */
$wp_version = '5.8-alpha-50992'; $wp_version = '5.8-alpha-50993';
/** /**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema. * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.

View File

@ -357,6 +357,7 @@ function is_registered_sidebar( $sidebar_id ) {
* @since 2.2.0 * @since 2.2.0
* @since 5.3.0 Formalized the existing and already documented `...$params` parameter * @since 5.3.0 Formalized the existing and already documented `...$params` parameter
* by adding it to the function signature. * by adding it to the function signature.
* @since 5.8.0 Added show_instance_in_rest option.
* *
* @global array $wp_registered_widgets Uses stored registered widgets. * @global array $wp_registered_widgets Uses stored registered widgets.
* @global array $wp_registered_widget_controls Stores the registered widget controls (options). * @global array $wp_registered_widget_controls Stores the registered widget controls (options).
@ -369,10 +370,12 @@ function is_registered_sidebar( $sidebar_id ) {
* @param array $options { * @param array $options {
* Optional. An array of supplementary widget options for the instance. * Optional. An array of supplementary widget options for the instance.
* *
* @type string $classname Class name for the widget's HTML container. Default is a shortened * @type string $classname Class name for the widget's HTML container. Default is a shortened
* version of the output callback name. * version of the output callback name.
* @type string $description Widget description for display in the widget administration * @type string $description Widget description for display in the widget administration
* panel and/or theme. * panel and/or theme.
* @type bool $show_instance_in_rest Whether to show the widget's instance settings in the REST API.
* Only available for WP_Widget based widgets.
* } * }
* @param mixed ...$params Optional additional parameters to pass to the callback function when it's called. * @param mixed ...$params Optional additional parameters to pass to the callback function when it's called.
*/ */
@ -1796,6 +1799,8 @@ function wp_widgets_init() {
register_widget( 'WP_Widget_Custom_HTML' ); register_widget( 'WP_Widget_Custom_HTML' );
register_widget( 'WP_Widget_Block' );
/** /**
* Fires after all default WordPress widgets have been registered. * Fires after all default WordPress widgets have been registered.
* *
@ -1803,3 +1808,166 @@ function wp_widgets_init() {
*/ */
do_action( 'widgets_init' ); do_action( 'widgets_init' );
} }
/**
* Converts a widget ID into its id_base and number components.
*
* @since 5.8.0
*
* @param string $id Widget ID.
* @return array Array containing a widget's id_base and number components.
*/
function wp_parse_widget_id( $id ) {
$parsed = array();
if ( preg_match( '/^(.+)-(\d+)$/', $id, $matches ) ) {
$parsed['id_base'] = $matches[1];
$parsed['number'] = (int) $matches[2];
} else {
// Likely an old single widget.
$parsed['id_base'] = $id;
}
return $parsed;
}
/**
* Finds the sidebar that a given widget belongs to.
*
* @since 5.8.0
*
* @param string $widget_id The widget id to look for.
* @return string|null The found sidebar's id, or null if it was not found.
*/
function wp_find_widgets_sidebar( $widget_id ) {
foreach ( wp_get_sidebars_widgets() as $sidebar_id => $widget_ids ) {
foreach ( $widget_ids as $maybe_widget_id ) {
if ( $maybe_widget_id === $widget_id ) {
return (string) $sidebar_id;
}
}
}
return null;
}
/**
* Assigns a widget to the given sidebar.
*
* @since 5.8.0
*
* @param string $widget_id The widget id to assign.
* @param string $sidebar_id The sidebar id to assign to. If empty, the widget won't be added to any sidebar.
*/
function wp_assign_widget_to_sidebar( $widget_id, $sidebar_id ) {
$sidebars = wp_get_sidebars_widgets();
foreach ( $sidebars as $maybe_sidebar_id => $widgets ) {
foreach ( $widgets as $i => $maybe_widget_id ) {
if ( $widget_id === $maybe_widget_id && $sidebar_id !== $maybe_sidebar_id ) {
unset( $sidebars[ $maybe_sidebar_id ][ $i ] );
// We could technically break 2 here, but continue looping in case the id is duplicated.
continue 2;
}
}
}
if ( $sidebar_id ) {
$sidebars[ $sidebar_id ][] = $widget_id;
}
wp_set_sidebars_widgets( $sidebars );
}
/**
* Calls the render callback of a widget and returns the output.
*
* @since 5.8.0
*
* @param string $widget_id Widget ID.
* @param string $sidebar_id Sidebar ID.
* @return string
*/
function wp_render_widget( $widget_id, $sidebar_id ) {
global $wp_registered_widgets, $wp_registered_sidebars;
if ( ! isset( $wp_registered_widgets[ $widget_id ] ) ) {
return '';
}
if ( isset( $wp_registered_sidebars[ $sidebar_id ] ) ) {
$sidebar = $wp_registered_sidebars[ $sidebar_id ];
} elseif ( 'wp_inactive_widgets' === $sidebar_id ) {
$sidebar = array();
} else {
return '';
}
$params = array_merge(
array(
array_merge(
$sidebar,
array(
'widget_id' => $widget_id,
'widget_name' => $wp_registered_widgets[ $widget_id ]['name'],
)
),
),
(array) $wp_registered_widgets[ $widget_id ]['params']
);
// Substitute HTML `id` and `class` attributes into `before_widget`.
$classname_ = '';
foreach ( (array) $wp_registered_widgets[ $widget_id ]['classname'] as $cn ) {
if ( is_string( $cn ) ) {
$classname_ .= '_' . $cn;
} elseif ( is_object( $cn ) ) {
$classname_ .= '_' . get_class( $cn );
}
}
$classname_ = ltrim( $classname_, '_' );
$params[0]['before_widget'] = sprintf( $params[0]['before_widget'], $widget_id, $classname_ );
/** This filter is documented in wp-includes/widgets.php */
$params = apply_filters( 'dynamic_sidebar_params', $params );
$callback = $wp_registered_widgets[ $widget_id ]['callback'];
ob_start();
/** This filter is documented in wp-includes/widgets.php */
do_action( 'dynamic_sidebar', $wp_registered_widgets[ $widget_id ] );
if ( is_callable( $callback ) ) {
call_user_func_array( $callback, $params );
}
return ob_get_clean();
}
/**
* Calls the control callback of a widget and returns the output.
*
* @since 5.8.0
*
* @param string $id Widget ID.
* @return string|null
*/
function wp_render_widget_control( $id ) {
global $wp_registered_widget_controls;
if ( ! isset( $wp_registered_widget_controls[ $id ]['callback'] ) ) {
return null;
}
$callback = $wp_registered_widget_controls[ $id ]['callback'];
$params = $wp_registered_widget_controls[ $id ]['params'];
ob_start();
if ( is_callable( $callback ) ) {
call_user_func_array( $callback, $params );
}
return ob_get_clean();
}

View File

@ -25,6 +25,7 @@ class WP_Nav_Menu_Widget extends WP_Widget {
$widget_ops = array( $widget_ops = array(
'description' => __( 'Add a navigation menu to your sidebar.' ), 'description' => __( 'Add a navigation menu to your sidebar.' ),
'customize_selective_refresh' => true, 'customize_selective_refresh' => true,
'show_instance_in_rest' => true,
); );
parent::__construct( 'nav_menu', __( 'Navigation Menu' ), $widget_ops ); parent::__construct( 'nav_menu', __( 'Navigation Menu' ), $widget_ops );
} }

View File

@ -26,6 +26,7 @@ class WP_Widget_Archives extends WP_Widget {
'classname' => 'widget_archive', 'classname' => 'widget_archive',
'description' => __( 'A monthly archive of your site&#8217;s Posts.' ), 'description' => __( 'A monthly archive of your site&#8217;s Posts.' ),
'customize_selective_refresh' => true, 'customize_selective_refresh' => true,
'show_instance_in_rest' => true,
); );
parent::__construct( 'archives', __( 'Archives' ), $widget_ops ); parent::__construct( 'archives', __( 'Archives' ), $widget_ops );
} }

View File

@ -0,0 +1,224 @@
<?php
/**
* Widget API: WP_Widget_Block class
*
* @package WordPress
* @subpackage Widgets
* @since 5.8.0
*/
/**
* Core class used to implement a Block widget.
*
* @since 5.8.0
*
* @see WP_Widget
*/
class WP_Widget_Block extends WP_Widget {
/**
* Default instance.
*
* @since 5.8.0
* @var array
*/
protected $default_instance = array(
'content' => '',
);
/**
* Sets up a new Block widget instance.
*
* @since 5.8.0
*/
public function __construct() {
$widget_ops = array(
'classname' => 'widget_block',
'description' => __( 'A widget containing a block.' ),
'customize_selective_refresh' => true,
'show_instance_in_rest' => true,
);
$control_ops = array(
'width' => 400,
'height' => 350,
);
parent::__construct( 'block', __( 'Block' ), $widget_ops, $control_ops );
add_filter( 'is_wide_widget_in_customizer', array( $this, 'set_is_wide_widget_in_customizer' ), 10, 2 );
}
/**
* Outputs the content for the current Block widget instance.
*
* @since 5.8.0
*
* @param array $args Display arguments including 'before_title', 'after_title',
* 'before_widget', and 'after_widget'.
* @param array $instance Settings for the current Block widget instance.
*
* @global WP_Post $post Global post object.
*/
public function widget( $args, $instance ) {
$instance = wp_parse_args( $instance, $this->default_instance );
echo str_replace(
'widget_block',
$this->get_dynamic_classname( $instance['content'] ),
$args['before_widget']
);
// 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;
$content = $wp_embed->run_shortcode( $instance['content'] );
$content = $wp_embed->autoembed( $content );
$content = do_blocks( $content );
$content = do_shortcode( $content );
echo $content;
echo $args['after_widget'];
}
/**
* Calculates the classname to use in the block widget's container HTML.
*
* Usually this is set to $this->widget_options['classname'] by
* dynamic_sidebar(). In this case, however, we want to set the classname
* dynamically depending on the block contained by this block widget.
*
* If a block widget contains a block that has an equivalent legacy widget,
* we display that legacy widget's class name. This helps with theme
* backwards compatibility.
*
* @since 5.8.0
*
* @param array $content The HTML content of the current block widget.
*
* @return string The classname to use in the block widget's container HTML.
*/
private function get_dynamic_classname( $content ) {
$blocks = parse_blocks( $content );
$block_name = isset( $blocks[0] ) ? $blocks[0]['blockName'] : null;
switch ( $block_name ) {
case 'core/paragraph':
$classname = 'widget_block widget_text';
break;
case 'core/calendar':
$classname = 'widget_block widget_calendar';
break;
case 'core/search':
$classname = 'widget_block widget_search';
break;
case 'core/html':
$classname = 'widget_block widget_custom_html';
break;
case 'core/archives':
$classname = 'widget_block widget_archive';
break;
case 'core/latest-posts':
$classname = 'widget_block widget_recent_entries';
break;
case 'core/latest-comments':
$classname = 'widget_block widget_recent_comments';
break;
case 'core/tag-cloud':
$classname = 'widget_block widget_tag_cloud';
break;
case 'core/categories':
$classname = 'widget_block widget_categories';
break;
case 'core/audio':
$classname = 'widget_block widget_media_audio';
break;
case 'core/video':
$classname = 'widget_block widget_media_video';
break;
case 'core/image':
$classname = 'widget_block widget_media_image';
break;
case 'core/gallery':
$classname = 'widget_block widget_media_gallery';
break;
case 'core/rss':
$classname = 'widget_block widget_rss';
break;
default:
$classname = 'widget_block';
}
/**
* The classname used in the block widget's container HTML.
*
* This can be set according to the name of the block contained by the
* block widget.
*
* @since 5.8.0
*
* @param string $classname The classname to be used in the block widget's container HTML, e.g. 'widget_block widget_text'.
* @param string $block_name The name of the block contained by the block widget, e.g. 'core/paragraph'.
*/
return apply_filters( 'widget_block_dynamic_classname', $classname, $block_name );
}
/**
* Handles updating settings for the current Block widget instance.
*
* @since 5.8.0
* @param array $new_instance New settings for this instance as input by the user via
* WP_Widget::form().
* @param array $old_instance Old settings for this instance.
*
* @return array Settings to save or bool false to cancel saving.
*/
public function update( $new_instance, $old_instance ) {
$instance = array_merge( $this->default_instance, $old_instance );
$instance['content'] = $new_instance['content'];
return $instance;
}
/**
* Outputs the Block widget settings form.
*
* @since 5.8.0
*
* @param array $instance Current instance.
*
* @see WP_Widget_Custom_HTML::render_control_template_scripts()
*/
public function form( $instance ) {
$instance = wp_parse_args( (array) $instance, $this->default_instance );
?>
<p>
<label for="<?php echo $this->get_field_id( 'content' ); ?>"><?php echo __( 'Block HTML:' ); ?></label>
<textarea id="<?php echo $this->get_field_id( 'content' ); ?>" name="<?php echo $this->get_field_name( 'content' ); ?>" rows="6" cols="50" class="widefat code"><?php echo esc_textarea( $instance['content'] ); ?></textarea>
</p>
<?php
}
/**
* Make sure no block widget is considered to be wide.
*
* @since 5.8.0
*
* @param boolean $is_wide Is regarded wide.
* @param string $widget_id Widget ID.
*
* @return bool Updated is_wide value.
*/
public function set_is_wide_widget_in_customizer( $is_wide, $widget_id ) {
if ( strpos( $widget_id, 'block-' ) === 0 ) {
return false;
}
return $is_wide;
}
}

View File

@ -33,6 +33,7 @@ class WP_Widget_Calendar extends WP_Widget {
'classname' => 'widget_calendar', 'classname' => 'widget_calendar',
'description' => __( 'A calendar of your sites posts.' ), 'description' => __( 'A calendar of your sites posts.' ),
'customize_selective_refresh' => true, 'customize_selective_refresh' => true,
'show_instance_in_rest' => true,
); );
parent::__construct( 'calendar', __( 'Calendar' ), $widget_ops ); parent::__construct( 'calendar', __( 'Calendar' ), $widget_ops );
} }

View File

@ -26,6 +26,7 @@ class WP_Widget_Categories extends WP_Widget {
'classname' => 'widget_categories', 'classname' => 'widget_categories',
'description' => __( 'A list or dropdown of categories.' ), 'description' => __( 'A list or dropdown of categories.' ),
'customize_selective_refresh' => true, 'customize_selective_refresh' => true,
'show_instance_in_rest' => true,
); );
parent::__construct( 'categories', __( 'Categories' ), $widget_ops ); parent::__construct( 'categories', __( 'Categories' ), $widget_ops );
} }

View File

@ -45,6 +45,7 @@ class WP_Widget_Custom_HTML extends WP_Widget {
'classname' => 'widget_custom_html', 'classname' => 'widget_custom_html',
'description' => __( 'Arbitrary HTML code.' ), 'description' => __( 'Arbitrary HTML code.' ),
'customize_selective_refresh' => true, 'customize_selective_refresh' => true,
'show_instance_in_rest' => true,
); );
$control_ops = array( $control_ops = array(
'width' => 400, 'width' => 400,

View File

@ -59,6 +59,7 @@ abstract class WP_Widget_Media extends WP_Widget {
array( array(
'description' => __( 'A media item.' ), 'description' => __( 'A media item.' ),
'customize_selective_refresh' => true, 'customize_selective_refresh' => true,
'show_instance_in_rest' => true,
'mime_type' => '', 'mime_type' => '',
) )
); );

View File

@ -28,6 +28,7 @@ class WP_Widget_Meta extends WP_Widget {
'classname' => 'widget_meta', 'classname' => 'widget_meta',
'description' => __( 'Login, RSS, &amp; WordPress.org links.' ), 'description' => __( 'Login, RSS, &amp; WordPress.org links.' ),
'customize_selective_refresh' => true, 'customize_selective_refresh' => true,
'show_instance_in_rest' => true,
); );
parent::__construct( 'meta', __( 'Meta' ), $widget_ops ); parent::__construct( 'meta', __( 'Meta' ), $widget_ops );
} }

View File

@ -26,6 +26,7 @@ class WP_Widget_Pages extends WP_Widget {
'classname' => 'widget_pages', 'classname' => 'widget_pages',
'description' => __( 'A list of your site&#8217;s Pages.' ), 'description' => __( 'A list of your site&#8217;s Pages.' ),
'customize_selective_refresh' => true, 'customize_selective_refresh' => true,
'show_instance_in_rest' => true,
); );
parent::__construct( 'pages', __( 'Pages' ), $widget_ops ); parent::__construct( 'pages', __( 'Pages' ), $widget_ops );
} }

View File

@ -26,6 +26,7 @@ class WP_Widget_Recent_Comments extends WP_Widget {
'classname' => 'widget_recent_comments', 'classname' => 'widget_recent_comments',
'description' => __( 'Your site&#8217;s most recent comments.' ), 'description' => __( 'Your site&#8217;s most recent comments.' ),
'customize_selective_refresh' => true, 'customize_selective_refresh' => true,
'show_instance_in_rest' => true,
); );
parent::__construct( 'recent-comments', __( 'Recent Comments' ), $widget_ops ); parent::__construct( 'recent-comments', __( 'Recent Comments' ), $widget_ops );
$this->alt_option_name = 'widget_recent_comments'; $this->alt_option_name = 'widget_recent_comments';

View File

@ -26,6 +26,7 @@ class WP_Widget_Recent_Posts extends WP_Widget {
'classname' => 'widget_recent_entries', 'classname' => 'widget_recent_entries',
'description' => __( 'Your site&#8217;s most recent Posts.' ), 'description' => __( 'Your site&#8217;s most recent Posts.' ),
'customize_selective_refresh' => true, 'customize_selective_refresh' => true,
'show_instance_in_rest' => true,
); );
parent::__construct( 'recent-posts', __( 'Recent Posts' ), $widget_ops ); parent::__construct( 'recent-posts', __( 'Recent Posts' ), $widget_ops );
$this->alt_option_name = 'widget_recent_entries'; $this->alt_option_name = 'widget_recent_entries';

View File

@ -25,6 +25,8 @@ class WP_Widget_RSS extends WP_Widget {
$widget_ops = array( $widget_ops = array(
'description' => __( 'Entries from any RSS or Atom feed.' ), 'description' => __( 'Entries from any RSS or Atom feed.' ),
'customize_selective_refresh' => true, 'customize_selective_refresh' => true,
'show_instance_in_rest' => true,
); );
$control_ops = array( $control_ops = array(
'width' => 400, 'width' => 400,

View File

@ -26,6 +26,7 @@ class WP_Widget_Search extends WP_Widget {
'classname' => 'widget_search', 'classname' => 'widget_search',
'description' => __( 'A search form for your site.' ), 'description' => __( 'A search form for your site.' ),
'customize_selective_refresh' => true, 'customize_selective_refresh' => true,
'show_instance_in_rest' => true,
); );
parent::__construct( 'search', _x( 'Search', 'Search widget' ), $widget_ops ); parent::__construct( 'search', _x( 'Search', 'Search widget' ), $widget_ops );
} }

View File

@ -25,6 +25,7 @@ class WP_Widget_Tag_Cloud extends WP_Widget {
$widget_ops = array( $widget_ops = array(
'description' => __( 'A cloud of your most used tags.' ), 'description' => __( 'A cloud of your most used tags.' ),
'customize_selective_refresh' => true, 'customize_selective_refresh' => true,
'show_instance_in_rest' => true,
); );
parent::__construct( 'tag_cloud', __( 'Tag Cloud' ), $widget_ops ); parent::__construct( 'tag_cloud', __( 'Tag Cloud' ), $widget_ops );
} }

View File

@ -34,6 +34,7 @@ class WP_Widget_Text extends WP_Widget {
'classname' => 'widget_text', 'classname' => 'widget_text',
'description' => __( 'Arbitrary text.' ), 'description' => __( 'Arbitrary text.' ),
'customize_selective_refresh' => true, 'customize_selective_refresh' => true,
'show_instance_in_rest' => true,
); );
$control_ops = array( $control_ops = array(
'width' => 400, 'width' => 400,

View File

@ -265,6 +265,9 @@ require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-plugins-controller.
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-block-directory-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-block-directory-controller.php';
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-application-passwords-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-application-passwords-controller.php';
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-site-health-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-site-health-controller.php';
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-sidebars-controller.php';
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-widget-types-controller.php';
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-widgets-controller.php';
require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-meta-fields.php'; require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-meta-fields.php';
require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-comment-meta-fields.php'; require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-comment-meta-fields.php';
require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-post-meta-fields.php'; require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-post-meta-fields.php';