diff --git a/src/wp-includes/class-wp-widget-factory.php b/src/wp-includes/class-wp-widget-factory.php index 588d6ca710980..82eda94e3a0a6 100644 --- a/src/wp-includes/class-wp-widget-factory.php +++ b/src/wp-includes/class-wp-widget-factory.php @@ -102,4 +102,22 @@ public function _register_widgets() { $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; + } } diff --git a/src/wp-includes/default-widgets.php b/src/wp-includes/default-widgets.php index 173573a3dd588..f8f3c4d141535 100644 --- a/src/wp-includes/default-widgets.php +++ b/src/wp-includes/default-widgets.php @@ -63,3 +63,6 @@ /** WP_Widget_Custom_HTML class */ 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'; diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index fef8f2ef10c95..c08c85b539550 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -313,6 +313,18 @@ function create_initial_rest_routes() { $controller = new WP_REST_Plugins_Controller(); $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. $controller = new WP_REST_Block_Directory_Controller(); $controller->register_routes(); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-sidebars-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-sidebars-controller.php new file mode 100644 index 0000000000000..7011018a83cab --- /dev/null +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-sidebars-controller.php @@ -0,0 +1,459 @@ +. + * + * @author Martin Pettersson + * @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[\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 ); + } +} diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-widget-types-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-widget-types-controller.php new file mode 100644 index 0000000000000..4fdd3ac1e2423 --- /dev/null +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-widget-types-controller.php @@ -0,0 +1,551 @@ +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[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[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' ) ), + ); + } +} diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-widgets-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-widgets-controller.php new file mode 100644 index 0000000000000..bf76cf509f3b1 --- /dev/null +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-widgets-controller.php @@ -0,0 +1,693 @@ +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[\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 ); + } +} diff --git a/src/wp-includes/widgets.php b/src/wp-includes/widgets.php index 6030ecc9fc970..5736467301844 100644 --- a/src/wp-includes/widgets.php +++ b/src/wp-includes/widgets.php @@ -357,6 +357,7 @@ function is_registered_sidebar( $sidebar_id ) { * @since 2.2.0 * @since 5.3.0 Formalized the existing and already documented `...$params` parameter * 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_widget_controls Stores the registered widget controls (options). @@ -369,10 +370,12 @@ function is_registered_sidebar( $sidebar_id ) { * @param array $options { * 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 - * version of the output callback name. - * @type string $description Widget description for display in the widget administration - * panel and/or theme. + * @type string $classname Class name for the widget's HTML container. Default is a shortened + * version of the output callback name. + * @type string $description Widget description for display in the widget administration + * 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. */ @@ -1796,6 +1799,8 @@ function wp_widgets_init() { register_widget( 'WP_Widget_Custom_HTML' ); + register_widget( 'WP_Widget_Block' ); + /** * Fires after all default WordPress widgets have been registered. * @@ -1803,3 +1808,166 @@ function wp_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(); +} diff --git a/src/wp-includes/widgets/class-wp-nav-menu-widget.php b/src/wp-includes/widgets/class-wp-nav-menu-widget.php index 3ec1cfbe4a27d..ea8626edb32d2 100644 --- a/src/wp-includes/widgets/class-wp-nav-menu-widget.php +++ b/src/wp-includes/widgets/class-wp-nav-menu-widget.php @@ -25,6 +25,7 @@ public function __construct() { $widget_ops = array( 'description' => __( 'Add a navigation menu to your sidebar.' ), 'customize_selective_refresh' => true, + 'show_instance_in_rest' => true, ); parent::__construct( 'nav_menu', __( 'Navigation Menu' ), $widget_ops ); } diff --git a/src/wp-includes/widgets/class-wp-widget-archives.php b/src/wp-includes/widgets/class-wp-widget-archives.php index 2d7d9c84e35f2..3f19ebadf7743 100644 --- a/src/wp-includes/widgets/class-wp-widget-archives.php +++ b/src/wp-includes/widgets/class-wp-widget-archives.php @@ -26,6 +26,7 @@ public function __construct() { 'classname' => 'widget_archive', 'description' => __( 'A monthly archive of your site’s Posts.' ), 'customize_selective_refresh' => true, + 'show_instance_in_rest' => true, ); parent::__construct( 'archives', __( 'Archives' ), $widget_ops ); } diff --git a/src/wp-includes/widgets/class-wp-widget-block.php b/src/wp-includes/widgets/class-wp-widget-block.php new file mode 100644 index 0000000000000..4c5abb38a4d74 --- /dev/null +++ b/src/wp-includes/widgets/class-wp-widget-block.php @@ -0,0 +1,224 @@ + '', + ); + + /** + * 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 ); + ?> +

+ + +

+ 'widget_calendar', 'description' => __( 'A calendar of your site’s posts.' ), 'customize_selective_refresh' => true, + 'show_instance_in_rest' => true, ); parent::__construct( 'calendar', __( 'Calendar' ), $widget_ops ); } diff --git a/src/wp-includes/widgets/class-wp-widget-categories.php b/src/wp-includes/widgets/class-wp-widget-categories.php index ce1164ebd37e2..e1a95c3636254 100644 --- a/src/wp-includes/widgets/class-wp-widget-categories.php +++ b/src/wp-includes/widgets/class-wp-widget-categories.php @@ -26,6 +26,7 @@ public function __construct() { 'classname' => 'widget_categories', 'description' => __( 'A list or dropdown of categories.' ), 'customize_selective_refresh' => true, + 'show_instance_in_rest' => true, ); parent::__construct( 'categories', __( 'Categories' ), $widget_ops ); } diff --git a/src/wp-includes/widgets/class-wp-widget-custom-html.php b/src/wp-includes/widgets/class-wp-widget-custom-html.php index 8ba3fdfc805fe..3f0d149dcbd0e 100644 --- a/src/wp-includes/widgets/class-wp-widget-custom-html.php +++ b/src/wp-includes/widgets/class-wp-widget-custom-html.php @@ -45,6 +45,7 @@ public function __construct() { 'classname' => 'widget_custom_html', 'description' => __( 'Arbitrary HTML code.' ), 'customize_selective_refresh' => true, + 'show_instance_in_rest' => true, ); $control_ops = array( 'width' => 400, diff --git a/src/wp-includes/widgets/class-wp-widget-media.php b/src/wp-includes/widgets/class-wp-widget-media.php index 09c2b0dd1fc96..25016fd268d0a 100644 --- a/src/wp-includes/widgets/class-wp-widget-media.php +++ b/src/wp-includes/widgets/class-wp-widget-media.php @@ -59,6 +59,7 @@ public function __construct( $id_base, $name, $widget_options = array(), $contro array( 'description' => __( 'A media item.' ), 'customize_selective_refresh' => true, + 'show_instance_in_rest' => true, 'mime_type' => '', ) ); diff --git a/src/wp-includes/widgets/class-wp-widget-meta.php b/src/wp-includes/widgets/class-wp-widget-meta.php index 5e58d0eb44603..6dd793512d21e 100644 --- a/src/wp-includes/widgets/class-wp-widget-meta.php +++ b/src/wp-includes/widgets/class-wp-widget-meta.php @@ -28,6 +28,7 @@ public function __construct() { 'classname' => 'widget_meta', 'description' => __( 'Login, RSS, & WordPress.org links.' ), 'customize_selective_refresh' => true, + 'show_instance_in_rest' => true, ); parent::__construct( 'meta', __( 'Meta' ), $widget_ops ); } diff --git a/src/wp-includes/widgets/class-wp-widget-pages.php b/src/wp-includes/widgets/class-wp-widget-pages.php index 566d02c3305d9..80636cfd41e97 100644 --- a/src/wp-includes/widgets/class-wp-widget-pages.php +++ b/src/wp-includes/widgets/class-wp-widget-pages.php @@ -26,6 +26,7 @@ public function __construct() { 'classname' => 'widget_pages', 'description' => __( 'A list of your site’s Pages.' ), 'customize_selective_refresh' => true, + 'show_instance_in_rest' => true, ); parent::__construct( 'pages', __( 'Pages' ), $widget_ops ); } diff --git a/src/wp-includes/widgets/class-wp-widget-recent-comments.php b/src/wp-includes/widgets/class-wp-widget-recent-comments.php index 77e4fe8851fb7..6461a17ad649b 100644 --- a/src/wp-includes/widgets/class-wp-widget-recent-comments.php +++ b/src/wp-includes/widgets/class-wp-widget-recent-comments.php @@ -26,6 +26,7 @@ public function __construct() { 'classname' => 'widget_recent_comments', 'description' => __( 'Your site’s most recent comments.' ), 'customize_selective_refresh' => true, + 'show_instance_in_rest' => true, ); parent::__construct( 'recent-comments', __( 'Recent Comments' ), $widget_ops ); $this->alt_option_name = 'widget_recent_comments'; diff --git a/src/wp-includes/widgets/class-wp-widget-recent-posts.php b/src/wp-includes/widgets/class-wp-widget-recent-posts.php index 4b2fd56532d39..ef8f4c462454b 100644 --- a/src/wp-includes/widgets/class-wp-widget-recent-posts.php +++ b/src/wp-includes/widgets/class-wp-widget-recent-posts.php @@ -26,6 +26,7 @@ public function __construct() { 'classname' => 'widget_recent_entries', 'description' => __( 'Your site’s most recent Posts.' ), 'customize_selective_refresh' => true, + 'show_instance_in_rest' => true, ); parent::__construct( 'recent-posts', __( 'Recent Posts' ), $widget_ops ); $this->alt_option_name = 'widget_recent_entries'; diff --git a/src/wp-includes/widgets/class-wp-widget-rss.php b/src/wp-includes/widgets/class-wp-widget-rss.php index 5d209ef919a31..0b07fd81015b0 100644 --- a/src/wp-includes/widgets/class-wp-widget-rss.php +++ b/src/wp-includes/widgets/class-wp-widget-rss.php @@ -25,6 +25,8 @@ public function __construct() { $widget_ops = array( 'description' => __( 'Entries from any RSS or Atom feed.' ), 'customize_selective_refresh' => true, + 'show_instance_in_rest' => true, + ); $control_ops = array( 'width' => 400, diff --git a/src/wp-includes/widgets/class-wp-widget-search.php b/src/wp-includes/widgets/class-wp-widget-search.php index 8faa1f3baf7c1..33837c50137bb 100644 --- a/src/wp-includes/widgets/class-wp-widget-search.php +++ b/src/wp-includes/widgets/class-wp-widget-search.php @@ -26,6 +26,7 @@ public function __construct() { 'classname' => 'widget_search', 'description' => __( 'A search form for your site.' ), 'customize_selective_refresh' => true, + 'show_instance_in_rest' => true, ); parent::__construct( 'search', _x( 'Search', 'Search widget' ), $widget_ops ); } diff --git a/src/wp-includes/widgets/class-wp-widget-tag-cloud.php b/src/wp-includes/widgets/class-wp-widget-tag-cloud.php index 8b5c4a80b9e0a..fb17e69a1679f 100644 --- a/src/wp-includes/widgets/class-wp-widget-tag-cloud.php +++ b/src/wp-includes/widgets/class-wp-widget-tag-cloud.php @@ -25,6 +25,7 @@ public function __construct() { $widget_ops = array( 'description' => __( 'A cloud of your most used tags.' ), 'customize_selective_refresh' => true, + 'show_instance_in_rest' => true, ); parent::__construct( 'tag_cloud', __( 'Tag Cloud' ), $widget_ops ); } diff --git a/src/wp-includes/widgets/class-wp-widget-text.php b/src/wp-includes/widgets/class-wp-widget-text.php index 5342512b81d24..f3e479819d5bc 100644 --- a/src/wp-includes/widgets/class-wp-widget-text.php +++ b/src/wp-includes/widgets/class-wp-widget-text.php @@ -34,6 +34,7 @@ public function __construct() { 'classname' => 'widget_text', 'description' => __( 'Arbitrary text.' ), 'customize_selective_refresh' => true, + 'show_instance_in_rest' => true, ); $control_ops = array( 'width' => 400, diff --git a/src/wp-settings.php b/src/wp-settings.php index 45949fda91198..770fa9ff3dcec 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -265,6 +265,9 @@ 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-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-comment-meta-fields.php'; require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-post-meta-fields.php'; diff --git a/tests/phpunit/tests/rest-api/rest-schema-setup.php b/tests/phpunit/tests/rest-api/rest-schema-setup.php index 042c778d678e9..ed608d57a7bcd 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-setup.php +++ b/tests/phpunit/tests/rest-api/rest-schema-setup.php @@ -134,6 +134,13 @@ public function test_expected_routes_in_schema() { '/wp/v2/plugins', '/wp/v2/plugins/(?P[^.\/]+(?:\/[^.\/]+)?)', '/wp/v2/block-directory/search', + '/wp/v2/sidebars', + '/wp/v2/sidebars/(?P[\w-]+)', + '/wp/v2/widget-types', + '/wp/v2/widget-types/(?P[a-zA-Z0-9_-]+)', + '/wp/v2/widget-types/(?P[a-zA-Z0-9_-]+)/encode', + '/wp/v2/widgets', + '/wp/v2/widgets/(?P[\w\-]+)', '/wp-site-health/v1', '/wp-site-health/v1/tests/background-updates', '/wp-site-health/v1/tests/loopback-requests', diff --git a/tests/phpunit/tests/rest-api/rest-sidebars-controller.php b/tests/phpunit/tests/rest-api/rest-sidebars-controller.php new file mode 100644 index 0000000000000..731e224964ab2 --- /dev/null +++ b/tests/phpunit/tests/rest-api/rest-sidebars-controller.php @@ -0,0 +1,627 @@ +user->create( + array( + 'role' => 'administrator', + ) + ); + self::$author_id = $factory->user->create( + array( + 'role' => 'author', + ) + ); + } + + public static function wpTearDownAfterClass() { + wp_delete_user( self::$admin_id ); + wp_delete_user( self::$author_id ); + } + + public function setUp() { + parent::setUp(); + + wp_set_current_user( self::$admin_id ); + + // Unregister all widgets and sidebars. + global $wp_registered_sidebars, $_wp_sidebars_widgets; + $wp_registered_sidebars = array(); + $_wp_sidebars_widgets = array(); + update_option( 'sidebars_widgets', array() ); + } + + private function setup_widget( $option_name, $number, $settings ) { + update_option( + $option_name, + array( + $number => $settings, + ) + ); + } + + private function setup_sidebar( $id, $attrs = array(), $widgets = array() ) { + global $wp_registered_sidebars; + update_option( + 'sidebars_widgets', + array( + $id => $widgets, + ) + ); + $wp_registered_sidebars[ $id ] = array_merge( + array( + 'id' => $id, + 'before_widget' => '', + 'after_widget' => '', + 'before_title' => '', + 'after_title' => '', + ), + $attrs + ); + + global $wp_registered_widgets; + foreach ( $wp_registered_widgets as $wp_registered_widget ) { + if ( is_array( $wp_registered_widget['callback'] ) ) { + $wp_registered_widget['callback'][0]->_register(); + } + } + } + + /** + * @ticket 41683 + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( '/wp/v2/sidebars', $routes ); + $this->assertArrayHasKey( '/wp/v2/sidebars/(?P[\w-]+)', $routes ); + } + + /** + * @ticket 41683 + */ + public function test_context_param() { + // Collection. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/sidebars' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); + $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); + // Single. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/sidebars/sidebar-1' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); + $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); + } + + /** + * @ticket 41683 + */ + public function test_get_items() { + $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( array(), $data ); + } + + /** + * @ticket 41683 + */ + public function test_get_items_no_permission() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); + } + + /** + * @ticket 41683 + */ + public function test_get_items_wrong_permission_author() { + wp_set_current_user( self::$author_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 403 ); + } + + /** + * @ticket 41683 + */ + public function test_get_items_basic_sidebar() { + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $data = $this->remove_links( $data ); + $this->assertEquals( + array( + array( + 'id' => 'sidebar-1', + 'name' => 'Test sidebar', + 'description' => '', + 'status' => 'active', + 'widgets' => array(), + 'class' => '', + 'before_widget' => '', + 'after_widget' => '', + 'before_title' => '', + 'after_title' => '', + ), + ), + $data + ); + } + + /** + * @ticket 41683 + */ + public function test_get_items_active_sidebar_with_widgets() { + $this->setup_widget( + 'widget_rss', + 1, + array( + 'title' => 'RSS test', + ) + ); + $this->setup_widget( + 'widget_text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( 'text-1', 'rss-1' ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $data = $this->remove_links( $data ); + $this->assertEquals( + array( + array( + 'id' => 'sidebar-1', + 'name' => 'Test sidebar', + 'description' => '', + 'status' => 'active', + 'widgets' => array( + 'text-1', + 'rss-1', + ), + 'class' => '', + 'before_widget' => '', + 'after_widget' => '', + 'before_title' => '', + 'after_title' => '', + ), + ), + $data + ); + } + + /** + * @ticket 41683 + */ + public function test_get_item() { + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars/sidebar-1' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $data = $this->remove_links( $data ); + $this->assertEquals( + array( + 'id' => 'sidebar-1', + 'name' => 'Test sidebar', + 'description' => '', + 'status' => 'active', + 'widgets' => array(), + 'class' => '', + 'before_widget' => '', + 'after_widget' => '', + 'before_title' => '', + 'after_title' => '', + ), + $data + ); + } + + /** + * @ticket 41683 + */ + public function test_get_item_no_permission() { + wp_set_current_user( 0 ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars/sidebar-1' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); + } + + /** + * @ticket 41683 + */ + public function test_get_item_wrong_permission_author() { + wp_set_current_user( self::$author_id ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars/sidebar-1' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 403 ); + } + + /** + * The test_create_item() method does not exist for sidebar. + */ + public function test_create_item() { + } + + /** + * @ticket 41683 + */ + public function test_update_item() { + $this->setup_widget( + 'widget_rss', + 1, + array( + 'title' => 'RSS test', + ) + ); + $this->setup_widget( + 'widget_text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_widget( + 'widget_text', + 2, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( 'text-1', 'rss-1' ) + ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/sidebars/sidebar-1' ); + $request->set_body_params( + array( + 'widgets' => array( + 'text-1', + 'text-2', + ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $data = $this->remove_links( $data ); + $this->assertEquals( + array( + 'id' => 'sidebar-1', + 'name' => 'Test sidebar', + 'description' => '', + 'status' => 'active', + 'widgets' => array( + 'text-1', + 'text-2', + ), + 'class' => '', + 'before_widget' => '', + 'after_widget' => '', + 'before_title' => '', + 'after_title' => '', + ), + $data + ); + } + + /** + * @ticket 41683 + */ + public function test_update_item_removes_widget_from_existing_sidebar() { + $this->setup_widget( + 'widget_text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( 'text-1' ) + ); + $this->setup_sidebar( + 'sidebar-2', + array( + 'name' => 'Test sidebar 2', + ), + array() + ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/sidebars/sidebar-2' ); + $request->set_body_params( + array( + 'widgets' => array( + 'text-1', + ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertContains( 'text-1', $data['widgets'] ); + + $this->assertNotContains( 'text-1', rest_do_request( '/wp/v2/sidebars/sidebar-1' )->get_data()['widgets'] ); + } + + /** + * @ticket 41683 + */ + public function test_update_item_moves_omitted_widget_to_inactive_sidebar() { + $this->setup_widget( + 'widget_text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_widget( + 'widget_text', + 2, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( 'text-1' ) + ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/sidebars/sidebar-1' ); + $request->set_body_params( + array( + 'widgets' => array( + 'text-2', + ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertContains( 'text-2', $data['widgets'] ); + $this->assertNotContains( 'text-1', $data['widgets'] ); + + $this->assertContains( 'text-1', rest_do_request( '/wp/v2/sidebars/wp_inactive_widgets' )->get_data()['widgets'] ); + } + + /** + * @ticket 41683 + */ + public function test_get_items_inactive_widgets() { + $this->setup_widget( + 'widget_rss', + 1, + array( + 'title' => 'RSS test', + ) + ); + $this->setup_widget( + 'widget_text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( 'text-1' ) + ); + update_option( + 'sidebars_widgets', + array_merge( + get_option( 'sidebars_widgets' ), + array( + 'wp_inactive_widgets' => array( 'rss-1', 'rss' ), + ) + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars' ); + $request->set_param( 'context', 'view' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $data = $this->remove_links( $data ); + $this->assertEquals( + array( + array( + 'id' => 'sidebar-1', + 'name' => 'Test sidebar', + 'description' => '', + 'status' => 'active', + 'widgets' => array( + 'text-1', + ), + 'class' => '', + 'before_widget' => '', + 'after_widget' => '', + 'before_title' => '', + 'after_title' => '', + ), + array( + 'id' => 'wp_inactive_widgets', + 'name' => 'Inactive widgets', + 'description' => '', + 'status' => 'inactive', + 'widgets' => array( + 'rss-1', + ), + 'class' => '', + 'before_widget' => '', + 'after_widget' => '', + 'before_title' => '', + 'after_title' => '', + ), + ), + $data + ); + } + + /** + * @ticket 41683 + */ + public function test_update_item_no_permission() { + wp_set_current_user( 0 ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/sidebars/sidebar-1' ); + $request->set_body_params( + array( + 'widgets' => array(), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); + } + + /** + * @ticket 41683 + */ + public function test_update_item_wrong_permission_author() { + wp_set_current_user( self::$author_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/sidebars/sidebar-1' ); + $request->set_body_params( + array( + 'widgets' => array(), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 403 ); + } + + /** + * The test_delete_item() method does not exist for sidebar. + */ + public function test_delete_item() { + } + + /** + * The test_prepare_item() method does not exist for sidebar. + */ + public function test_prepare_item() { + } + + /** + * @ticket 41683 + */ + public function test_get_item_schema() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/sidebars' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'name', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'status', $properties ); + $this->assertArrayHasKey( 'widgets', $properties ); + $this->assertArrayHasKey( 'class', $properties ); + $this->assertArrayHasKey( 'before_widget', $properties ); + $this->assertArrayHasKey( 'after_widget', $properties ); + $this->assertArrayHasKey( 'before_title', $properties ); + $this->assertArrayHasKey( 'after_title', $properties ); + $this->assertCount( 10, $properties ); + } + + /** + * Helper to remove links key. + * + * @param array $data Array of data. + * + * @return array + */ + protected function remove_links( $data ) { + if ( ! is_array( $data ) ) { + return $data; + } + $count = 0; + foreach ( $data as $item ) { + if ( isset( $item['_links'] ) ) { + unset( $data[ $count ]['_links'] ); + } + $count ++; + } + + return $data; + } +} diff --git a/tests/phpunit/tests/rest-api/rest-widget-types-controller.php b/tests/phpunit/tests/rest-api/rest-widget-types-controller.php new file mode 100644 index 0000000000000..884e0937f0545 --- /dev/null +++ b/tests/phpunit/tests/rest-api/rest-widget-types-controller.php @@ -0,0 +1,464 @@ +user->create( + array( + 'role' => 'administrator', + ) + ); + self::$subscriber_id = $factory->user->create( + array( + 'role' => 'subscriber', + ) + ); + } + + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + self::delete_user( self::$subscriber_id ); + } + + /** + * @ticket 41683 + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( '/wp/v2/widget-types', $routes ); + $this->assertCount( 1, $routes['/wp/v2/widget-types'] ); + $this->assertArrayHasKey( '/wp/v2/widget-types/(?P[a-zA-Z0-9_-]+)', $routes ); + $this->assertCount( 1, $routes['/wp/v2/widget-types/(?P[a-zA-Z0-9_-]+)'] ); + $this->assertArrayHasKey( '/wp/v2/widget-types/(?P[a-zA-Z0-9_-]+)/encode', $routes ); + $this->assertCount( 1, $routes['/wp/v2/widget-types/(?P[a-zA-Z0-9_-]+)/encode'] ); + } + + /** + * @ticket 41683 + */ + public function test_context_param() { + // Collection. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/widget-types' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); + $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); + // Single. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/widget-types/calendar' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); + $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); + } + + /** + * @ticket 41683 + */ + public function test_get_items() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/widget-types' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertGreaterThan( 1, count( $data ) ); + $endpoint = new WP_REST_Widget_Types_Controller; + foreach ( $data as $item ) { + $widget_type = $endpoint->get_widget( $item['name'] ); + $this->check_widget_type_object( $widget_type, $item, $item['_links'] ); + } + + } + + /** + * @ticket 41683 + */ + public function test_get_item() { + $widget_name = 'calendar'; + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/widget-types/' . $widget_name ); + $response = rest_get_server()->dispatch( $request ); + $endpoint = new WP_REST_Widget_Types_Controller; + $widget_type = $endpoint->get_widget( $widget_name ); + $this->check_widget_type_object( $widget_type, $response->get_data(), $response->get_links() ); + } + + /** + * @ticket 41683 + */ + public function test_get_widget_legacy() { + $widget_id = 'legacy'; + wp_register_sidebar_widget( + $widget_id, + 'WP legacy widget', + function() {} + ); + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/widget-types/' . $widget_id ); + $response = rest_get_server()->dispatch( $request ); + $endpoint = new WP_REST_Widget_Types_Controller; + $widget_type = $endpoint->get_widget( $widget_id ); + $this->check_widget_type_object( $widget_type, $response->get_data(), $response->get_links() ); + } + + /** + * @ticket 41683 + */ + public function test_get_widget_invalid_name() { + $widget_type = 'fake'; + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/widget-types/' . $widget_type ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_widget_type_invalid', $response, 404 ); + } + + /** + * @ticket 41683 + */ + public function test_get_item_schema() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/widget-types' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + $this->assertCount( 5, $properties ); + + $this->assertArrayHasKey( 'name', $properties ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'is_multi', $properties ); + $this->assertArrayHasKey( 'classname', $properties ); + } + + /** + * @ticket 41683 + */ + public function test_get_items_wrong_permission() { + wp_set_current_user( self::$subscriber_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/widget-types' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 403 ); + } + + /** + * @ticket 41683 + */ + public function test_get_item_wrong_permission() { + wp_set_current_user( self::$subscriber_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/widget-types/calendar' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 403 ); + } + + /** + * @ticket 41683 + */ + public function test_get_items_no_permission() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/widget-types' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); + } + + /** + * @ticket 41683 + */ + public function test_get_item_no_permission() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/widget-types/calendar' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); + } + + /** + * @ticket 41683 + */ + public function test_prepare_item() { + $endpoint = new WP_REST_Widget_Types_Controller; + $widget_type = $endpoint->get_widget( 'calendar' ); + $request = new WP_REST_Request; + $request->set_param( 'context', 'edit' ); + $response = $endpoint->prepare_item_for_response( $widget_type, $request ); + $this->check_widget_type_object( $widget_type, $response->get_data(), $response->get_links() ); + } + + /** + * Util check widget type object against. + * + * @since 5.8.0 + * + * @param array $widget_type Sample widget type. + * @param array $data Data to compare against. + * @param array $links Links to compare again. + */ + protected function check_widget_type_object( $widget_type, $data, $links ) { + // Test data. + $extra_fields = array( + 'name', + 'id_base', + 'option_name', + 'control_options', + 'widget_options', + 'widget_class', + 'is_multi', + ); + + foreach ( $extra_fields as $extra_field ) { + if ( isset( $widget_type->$extra_field ) ) { + $this->assertSame( $data[ $extra_field ], $widget_type->$extra_field, 'Field ' . $extra_field ); + } + } + + // Test links. + $this->assertSame( rest_url( 'wp/v2/widget-types' ), $links['collection'][0]['href'] ); + } + + /** + * @ticket 41683 + */ + public function test_encode_form_data_with_no_input() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/widget-types/search/encode' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( + "

\n" . + "\t\t\t\n" . + "\t\t\t\n" . + "\t\t

", + $data['form'] + ); + $this->assertStringMatchesFormat( + "
\n" . + "\t\t\t\t
\n" . + "\t\t\t\t\t\n" . + "\t\t\t\t\t\n" . + "\t\t\t\t\t\n" . + "\t\t\t\t
\n" . + "\t\t\t
", + $data['preview'] + ); + $this->assertEqualSets( + array( + 'encoded' => base64_encode( serialize( array() ) ), + 'hash' => wp_hash( serialize( array() ) ), + 'raw' => new stdClass, + ), + $data['instance'] + ); + } + + /** + * @ticket 41683 + */ + public function test_encode_form_data_with_number() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/widget-types/search/encode' ); + $request->set_param( 'number', 8 ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( + "

\n" . + "\t\t\t\n" . + "\t\t\t\n" . + "\t\t

", + $data['form'] + ); + $this->assertStringMatchesFormat( + "
\n" . + "\t\t\t\t
\n" . + "\t\t\t\t\t\n" . + "\t\t\t\t\t\n" . + "\t\t\t\t\t\n" . + "\t\t\t\t
\n" . + "\t\t\t
", + $data['preview'] + ); + $this->assertEqualSets( + array( + 'encoded' => base64_encode( serialize( array() ) ), + 'hash' => wp_hash( serialize( array() ) ), + 'raw' => new stdClass, + ), + $data['instance'] + ); + } + + /** + * @ticket 41683 + */ + public function test_encode_form_data_with_instance() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/widget-types/search/encode' ); + $request->set_param( + 'instance', + array( + 'encoded' => base64_encode( serialize( array( 'title' => 'Test title' ) ) ), + 'hash' => wp_hash( serialize( array( 'title' => 'Test title' ) ) ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( + "

\n" . + "\t\t\t\n" . + "\t\t\t\n" . + "\t\t

", + $data['form'] + ); + $this->assertStringMatchesFormat( + "

Test title

\n" . + "\t\t\t\t
\n" . + "\t\t\t\t\t\n" . + "\t\t\t\t\t\n" . + "\t\t\t\t\t\n" . + "\t\t\t\t
\n" . + "\t\t\t
", + $data['preview'] + ); + $this->assertEqualSets( + array( + 'encoded' => base64_encode( serialize( array( 'title' => 'Test title' ) ) ), + 'hash' => wp_hash( serialize( array( 'title' => 'Test title' ) ) ), + 'raw' => array( 'title' => 'Test title' ), + ), + $data['instance'] + ); + } + + /** + * @ticket 41683 + */ + public function test_encode_form_data_with_form_data() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/widget-types/search/encode' ); + $request->set_param( 'form_data', 'widget-search[-1][title]=Updated+title' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( + "

\n" . + "\t\t\t\n" . + "\t\t\t\n" . + "\t\t

", + $data['form'] + ); + $this->assertStringMatchesFormat( + "

Updated title

\n" . + "\t\t\t\t
\n" . + "\t\t\t\t\t\n" . + "\t\t\t\t\t\n" . + "\t\t\t\t\t\n" . + "\t\t\t\t
\n" . + "\t\t\t
", + $data['preview'] + ); + $this->assertEqualSets( + array( + 'encoded' => base64_encode( serialize( array( 'title' => 'Updated title' ) ) ), + 'hash' => wp_hash( serialize( array( 'title' => 'Updated title' ) ) ), + 'raw' => array( 'title' => 'Updated title' ), + ), + $data['instance'] + ); + } + + /** + * @ticket 41683 + */ + public function test_encode_form_data_no_raw() { + global $wp_widget_factory; + wp_set_current_user( self::$admin_id ); + $wp_widget_factory->widgets['WP_Widget_Search']->widget_options['show_instance_in_rest'] = false; + $request = new WP_REST_Request( 'POST', '/wp/v2/widget-types/search/encode' ); + $request->set_param( + 'instance', + array( + 'encoded' => base64_encode( serialize( array( 'title' => 'Test title' ) ) ), + 'hash' => wp_hash( serialize( array( 'title' => 'Test title' ) ) ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( + "

\n" . + "\t\t\t\n" . + "\t\t\t\n" . + "\t\t

", + $data['form'] + ); + $this->assertStringMatchesFormat( + "

Test title

\n" . + "\t\t\t\t
\n" . + "\t\t\t\t\t\n" . + "\t\t\t\t\t\n" . + "\t\t\t\t\t\n" . + "\t\t\t\t
\n" . + "\t\t\t
", + $data['preview'] + ); + $this->assertEqualSets( + array( + 'encoded' => base64_encode( serialize( array( 'title' => 'Test title' ) ) ), + 'hash' => wp_hash( serialize( array( 'title' => 'Test title' ) ) ), + ), + $data['instance'] + ); + $wp_widget_factory->widgets['WP_Widget_Search']->widget_options['show_instance_in_rest'] = true; + } + + + /** + * The test_create_item() method does not exist for widget types. + */ + public function test_create_item() {} + + /** + * The test_update_item() method does not exist for widget types. + */ + public function test_update_item() {} + + /** + * The test_delete_item() method does not exist for widget types. + */ + public function test_delete_item() {} +} diff --git a/tests/phpunit/tests/rest-api/rest-widgets-controller.php b/tests/phpunit/tests/rest-api/rest-widgets-controller.php new file mode 100644 index 0000000000000..116e2f6a359a2 --- /dev/null +++ b/tests/phpunit/tests/rest-api/rest-widgets-controller.php @@ -0,0 +1,1388 @@ +user->create( + array( + 'role' => 'administrator', + 'user_login' => 'superadmin', + ) + ); + if ( is_multisite() ) { + update_site_option( 'site_admins', array( 'superadmin' ) ); + } + self::$admin_id = $factory->user->create( + array( + 'role' => 'administrator', + ) + ); + self::$editor_id = $factory->user->create( + array( + 'role' => 'editor', + ) + ); + self::$author_id = $factory->user->create( + array( + 'role' => 'author', + ) + ); + self::$subscriber_id = $factory->user->create( + array( + 'role' => 'subscriber', + ) + ); + } + + public function setUp() { + global $wp_registered_widgets, $wp_registered_sidebars, $_wp_sidebars_widgets, $wp_widget_factory; + + parent::setUp(); + + wp_set_current_user( self::$admin_id ); + + // Unregister all widgets and sidebars. + $wp_registered_widgets = array(); + $wp_registered_sidebars = array(); + $_wp_sidebars_widgets = array(); + update_option( 'sidebars_widgets', array() ); + + // Re-register core widgets. + $wp_widget_factory->_register_widgets(); + + // Register a non-multi widget for testing. + wp_register_widget_control( + 'testwidget', + 'WP test widget', + function () { + $settings = get_option( 'widget_testwidget' ); + + // check if anything's been sent. + if ( isset( $_POST['update_testwidget'] ) ) { + $settings['id'] = $_POST['test_id']; + $settings['title'] = $_POST['test_title']; + + update_option( 'widget_testwidget', $settings ); + } + + echo 'WP test widget form'; + }, + 100, + 200 + ); + wp_register_sidebar_widget( + 'testwidget', + 'WP test widget', + function () { + $settings = wp_parse_args( + get_option( 'widget_testwidget' ), + array( + 'id' => 'Default id', + 'title' => 'Default text', + ) + ); + echo '

' . $settings['id'] . '

' . $settings['title'] . ''; + }, + array( + 'description' => 'A non-multi widget for testing.', + ) + ); + } + + private function setup_widget( $id_base, $number, $settings ) { + global $wp_widget_factory; + + $option_name = "widget_$id_base"; + update_option( + $option_name, + array( + $number => $settings, + ) + ); + + $widget_object = $wp_widget_factory->get_widget_object( $id_base ); + $widget_object->_set( $number ); + $widget_object->_register_one( $number ); + } + + private function setup_sidebar( $id, $attrs = array(), $widgets = array() ) { + global $wp_registered_sidebars; + update_option( + 'sidebars_widgets', + array_merge( + (array) get_option( 'sidebars_widgets', array() ), + array( + $id => $widgets, + ) + ) + ); + $wp_registered_sidebars[ $id ] = array_merge( + array( + 'id' => $id, + 'before_widget' => '', + 'after_widget' => '', + 'before_title' => '', + 'after_title' => '', + ), + $attrs + ); + } + + /** + * @ticket 41683 + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( '/wp/v2/widgets', $routes ); + $this->assertArrayHasKey( '/wp/v2/widgets/(?P[\w\-]+)', $routes ); + } + + /** + * @ticket 41683 + */ + public function test_context_param() { + } + + /** + * @ticket 41683 + */ + public function test_get_items_no_widgets() { + $request = new WP_REST_Request( 'GET', '/wp/v2/widgets' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( array(), $data ); + } + + /** + * @ticket 41683 + */ + public function test_get_items_no_permission() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/widgets' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); + } + + /** + * @ticket 41683 + */ + public function test_get_items_wrong_permission_author() { + wp_set_current_user( self::$author_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/widgets' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 403 ); + } + + /** + * @ticket 41683 + */ + public function test_get_items() { + global $wp_widget_factory; + + $wp_widget_factory->widgets['WP_Widget_RSS']->widget_options['show_instance_in_rest'] = false; + + $block_content = '

Block test

'; + + $this->setup_widget( + 'rss', + 1, + array( + 'title' => 'RSS test', + ) + ); + $this->setup_widget( + 'block', + 1, + array( + 'content' => $block_content, + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( 'block-1', 'rss-1', 'testwidget' ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/widgets' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $data = $this->remove_links( $data ); + $this->assertEqualSets( + array( + array( + 'id' => 'block-1', + 'sidebar' => 'sidebar-1', + 'instance' => array( + 'encoded' => base64_encode( + serialize( + array( + 'content' => $block_content, + ) + ) + ), + 'hash' => wp_hash( + serialize( + array( + 'content' => $block_content, + ) + ) + ), + 'raw' => array( + 'content' => $block_content, + ), + ), + 'id_base' => 'block', + 'rendered' => '

Block test

', + ), + array( + 'id' => 'rss-1', + 'sidebar' => 'sidebar-1', + 'instance' => array( + 'encoded' => base64_encode( + serialize( + array( + 'title' => 'RSS test', + ) + ) + ), + 'hash' => wp_hash( + serialize( + array( + 'title' => 'RSS test', + ) + ) + ), + ), + 'id_base' => 'rss', + 'rendered' => '', + ), + array( + 'id' => 'testwidget', + 'sidebar' => 'sidebar-1', + 'instance' => null, + 'id_base' => 'testwidget', + 'rendered' => '

Default id

Default text', + ), + ), + $data + ); + + $wp_widget_factory->widgets['WP_Widget_RSS']->widget_options['show_instance_in_rest'] = true; + } + + /** + * Test a GET request in edit context. In particular, we expect rendered_form to be served correctly. + * + * @ticket 41683 + */ + public function test_get_items_edit_context() { + $this->setup_widget( + 'text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( 'text-1', 'testwidget' ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/widgets' ); + $request['context'] = 'edit'; + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $data = $this->remove_links( $data ); + $this->assertEqualSets( + array( + array( + 'id' => 'text-1', + 'sidebar' => 'sidebar-1', + 'instance' => array( + 'encoded' => base64_encode( + serialize( + array( + 'text' => 'Custom text test', + ) + ) + ), + 'hash' => wp_hash( + serialize( + array( + 'text' => 'Custom text test', + ) + ) + ), + 'raw' => array( + 'text' => 'Custom text test', + ), + ), + 'id_base' => 'text', + 'rendered' => '
Custom text test
', + 'rendered_form' => '' . "\n" . + ' ' . "\n" . + ' ' . "\n" . + ' ', + ), + array( + 'id' => 'testwidget', + 'sidebar' => 'sidebar-1', + 'instance' => null, + 'id_base' => 'testwidget', + 'rendered' => '

Default id

Default text', + 'rendered_form' => 'WP test widget form', + ), + ), + $data + ); + } + + /** + * @ticket 41683 + */ + public function test_get_item() { + $this->setup_widget( + 'text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( 'text-1' ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/widgets/text-1' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEqualSets( + array( + 'id' => 'text-1', + 'sidebar' => 'sidebar-1', + 'instance' => array( + 'encoded' => base64_encode( + serialize( + array( + 'text' => 'Custom text test', + ) + ) + ), + 'hash' => wp_hash( + serialize( + array( + 'text' => 'Custom text test', + ) + ) + ), + 'raw' => array( + 'text' => 'Custom text test', + ), + ), + 'id_base' => 'text', + 'rendered' => '
Custom text test
', + ), + $data + ); + } + + /** + * @ticket 41683 + */ + public function test_get_item_no_permission() { + wp_set_current_user( 0 ); + + $this->setup_widget( + 'text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( 'text-1' ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/widgets/text-1' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); + } + + /** + * @ticket 41683 + */ + public function test_get_item_wrong_permission_author() { + wp_set_current_user( self::$author_id ); + $this->setup_widget( + 'text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/widgets/text-1' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 403 ); + } + + /** + * @ticket 41683 + */ + public function test_create_item() { + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ) + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/widgets' ); + $request->set_body_params( + array( + 'sidebar' => 'sidebar-1', + 'instance' => array( + 'encoded' => base64_encode( + serialize( + array( + 'text' => 'Updated text test', + ) + ) + ), + 'hash' => wp_hash( + serialize( + array( + 'text' => 'Updated text test', + ) + ) + ), + ), + 'id_base' => 'text', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 'text-2', $data['id'] ); + $this->assertEquals( 'sidebar-1', $data['sidebar'] ); + $this->assertEqualSets( + array( + 'text' => 'Updated text test', + 'title' => '', + 'filter' => false, + ), + get_option( 'widget_text' )[2] + ); + } + + /** + * @ticket 41683 + */ + public function test_create_item_malformed_instance() { + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ) + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/widgets' ); + $request->set_body_params( + array( + 'sidebar' => 'sidebar-1', + 'instance' => array( + 'encoded' => base64_encode( + serialize( + array( + 'text' => 'Updated text test', + ) + ) + ), + 'hash' => 'badhash', + ), + 'id_base' => 'text', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_widget', $response, 400 ); + } + + /** + * @ticket 41683 + */ + public function test_create_item_bad_instance() { + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ) + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/widgets' ); + $request->set_body_params( + array( + 'sidebar' => 'sidebar-1', + 'instance' => array(), + 'id_base' => 'text', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_widget', $response, 400 ); + } + + /** + * @ticket 41683 + */ + public function test_create_item_using_raw_instance() { + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ) + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/widgets' ); + $request->set_body_params( + array( + 'sidebar' => 'sidebar-1', + 'instance' => array( + 'raw' => array( + 'content' => '

Block test

', + ), + ), + 'id_base' => 'block', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 'block-2', $data['id'] ); + $this->assertEquals( 'sidebar-1', $data['sidebar'] ); + $this->assertEqualSets( + array( + 'content' => '

Block test

', + ), + get_option( 'widget_block' )[2] + ); + } + + /** + * @ticket 41683 + */ + public function test_create_item_raw_instance_not_supported() { + global $wp_widget_factory; + + $wp_widget_factory->widgets['WP_Widget_Text']->widget_options['show_instance_in_rest'] = false; + + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ) + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/widgets' ); + $request->set_body_params( + array( + 'sidebar' => 'sidebar-1', + 'instance' => array( + 'raw' => array( + 'title' => 'Updated text test', + ), + ), + 'id_base' => 'text', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_widget', $response, 400 ); + + $wp_widget_factory->widgets['WP_Widget_Text']->widget_options['show_instance_in_rest'] = true; + } + + /** + * @ticket 41683 + */ + public function test_create_item_using_form_data() { + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ) + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/widgets' ); + $request->set_body_params( + array( + 'sidebar' => 'sidebar-1', + 'form_data' => 'widget-text[2][text]=Updated+text+test', + 'id_base' => 'text', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 'text-2', $data['id'] ); + $this->assertEquals( 'sidebar-1', $data['sidebar'] ); + $this->assertEqualSets( + array( + 'text' => 'Updated text test', + 'title' => '', + 'filter' => false, + ), + $data['instance']['raw'] + ); + } + + /** + * @ticket 41683 + */ + public function test_create_item_multiple_in_a_row() { + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ) + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/widgets' ); + $request->set_body_params( + array( + 'sidebar' => 'sidebar-1', + 'instance' => array( + 'raw' => array( 'text' => 'Text 1' ), + ), + 'id_base' => 'text', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 'text-2', $data['id'] ); + $this->assertEquals( 'sidebar-1', $data['sidebar'] ); + $this->assertEqualSets( + array( + 'text' => 'Text 1', + 'title' => '', + 'filter' => false, + ), + $data['instance']['raw'] + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/widgets' ); + $request->set_body_params( + array( + 'sidebar' => 'sidebar-1', + 'instance' => array( + 'raw' => array( 'text' => 'Text 2' ), + ), + 'id_base' => 'text', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 'text-3', $data['id'] ); + $this->assertEquals( 'sidebar-1', $data['sidebar'] ); + $this->assertEqualSets( + array( + 'text' => 'Text 2', + 'title' => '', + 'filter' => false, + ), + $data['instance']['raw'] + ); + + $sidebar = rest_do_request( '/wp/v2/sidebars/sidebar-1' ); + $this->assertContains( 'text-2', $sidebar->get_data()['widgets'] ); + $this->assertContains( 'text-3', $sidebar->get_data()['widgets'] ); + } + + /** + * @ticket 41683 + */ + public function test_create_item_second_instance() { + $this->setup_widget( + 'text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ) + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/widgets' ); + $request->set_body_params( + array( + 'sidebar' => 'sidebar-1', + 'instance' => array( + 'raw' => array( + 'text' => 'Updated text test', + ), + ), + 'id_base' => 'text', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 'text-2', $data['id'] ); + $this->assertEquals( 'sidebar-1', $data['sidebar'] ); + $this->assertEqualSets( + array( + 'text' => 'Updated text test', + 'title' => '', + 'filter' => false, + ), + $data['instance']['raw'] + ); + } + + /** + * @ticket 41683 + */ + public function test_update_item() { + $this->setup_widget( + 'text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( 'text-1', 'rss-1' ) + ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/widgets/text-1' ); + $request->set_body_params( + array( + 'id' => 'text-1', + 'sidebar' => 'sidebar-1', + 'instance' => array( + 'raw' => array( + 'text' => 'Updated text test', + ), + ), + 'id_base' => 'text', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'text-1', $data['id'] ); + $this->assertEquals( 'sidebar-1', $data['sidebar'] ); + $this->assertEqualSets( + array( + 'text' => 'Updated text test', + 'title' => '', + 'filter' => false, + ), + $data['instance']['raw'] + ); + } + + /** + * @ticket 41683 + */ + public function test_update_item_reassign_sidebar() { + $this->setup_widget( + 'text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( 'text-1', 'rss-1' ) + ); + $this->setup_sidebar( + 'sidebar-2', + array( + 'name' => 'Test sidebar', + ), + array() + ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/widgets/text-1' ); + $request->set_body_params( + array( + 'sidebar' => 'sidebar-2', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $error = $response->as_error(); + $this->assertNotWPError( $error, $error ? $error->get_error_message() : '' ); + $this->assertEquals( 'sidebar-2', $response->get_data()['sidebar'] ); + + $sidebar1 = rest_do_request( '/wp/v2/sidebars/sidebar-1' ); + $this->assertNotContains( 'text-1', $sidebar1->get_data()['widgets'] ); + + $sidebar2 = rest_do_request( '/wp/v2/sidebars/sidebar-2' ); + $this->assertContains( 'text-1', $sidebar2->get_data()['widgets'] ); + } + + /** + * @ticket 41683 + */ + public function test_update_item_shouldnt_require_id_base() { + $this->setup_widget( + 'text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( 'text-1', 'rss-1' ) + ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/widgets/text-1' ); + $request->set_body_params( + array( + 'id' => 'text-1', + 'instance' => array( + 'raw' => array( + 'text' => 'Updated text test', + ), + ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'text-1', $data['id'] ); + $this->assertEquals( 'sidebar-1', $data['sidebar'] ); + $this->assertEqualSets( + array( + 'text' => 'Updated text test', + 'title' => '', + 'filter' => false, + ), + $data['instance']['raw'] + ); + } + + /** + * @group multisite + */ + public function test_store_html_as_admin() { + if ( is_multisite() ) { + $this->assertEquals( + '
alert(1)
', + $this->update_text_widget_with_raw_html( '' ) + ); + } else { + $this->assertEquals( + '
', + $this->update_text_widget_with_raw_html( '' ) + ); + } + } + + /** + * @group multisite + */ + public function test_store_html_as_superadmin() { + wp_set_current_user( self::$superadmin_id ); + if ( is_multisite() ) { + $this->assertEquals( + '
', + $this->update_text_widget_with_raw_html( '' ) + ); + } else { + $this->assertEquals( + '
', + $this->update_text_widget_with_raw_html( '' ) + ); + } + } + + protected function update_text_widget_with_raw_html( $html ) { + $this->setup_widget( + 'text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( 'text-1' ) + ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/widgets/text-1' ); + $request->set_body_params( + array( + 'id' => 'text-1', + 'instance' => array( + 'raw' => array( + 'text' => $html, + ), + ), + 'id_base' => 'text', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + return $data['rendered']; + } + + /** + * @ticket 41683 + */ + public function test_update_item_legacy_widget() { + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( 'testwidget' ) + ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/widgets/testwidget' ); + $request->set_body_params( + array( + 'id' => 'testwidget', + 'name' => 'WP test widget', + 'form_data' => 'test_id=My+test+id&test_title=My+test+title&update_testwidget=true', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $data = $this->remove_links( $data ); + $this->assertEquals( + array( + 'id' => 'testwidget', + 'sidebar' => 'sidebar-1', + 'instance' => null, + 'rendered' => '

My test id

My test title', + 'rendered_form' => 'WP test widget form', + 'id_base' => 'testwidget', + ), + $data + ); + } + + /** + * @ticket 41683 + */ + public function test_create_item_legacy_widget() { + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array() + ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/widgets/testwidget' ); + $request->set_body_params( + array( + 'id' => 'testwidget', + 'sidebar' => 'sidebar-1', + 'name' => 'WP test widget', + 'form_data' => 'test_id=My+test+id&test_title=My+test+title&update_testwidget=true', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $data = $this->remove_links( $data ); + $this->assertEquals( + array( + 'id' => 'testwidget', + 'sidebar' => 'sidebar-1', + 'instance' => null, + 'rendered' => '

My test id

My test title', + 'rendered_form' => 'WP test widget form', + 'id_base' => 'testwidget', + ), + $data + ); + } + + /** + * @ticket 41683 + */ + public function test_update_item_no_permission() { + wp_set_current_user( 0 ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/sidebars/sidebar-1' ); + $request->set_body_params( + array( + 'widgets' => array(), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); + } + + /** + * @ticket 41683 + */ + public function test_update_item_wrong_permission_author() { + wp_set_current_user( self::$author_id ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/sidebars/sidebar-1' ); + $request->set_body_params( + array( + 'widgets' => array(), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 403 ); + } + + /** + * Tests if the endpoint correctly handles "slashable" characters such as " or '. + */ + public function test_update_item_slashing() { + $this->setup_widget( 'text', 1, array( 'text' => 'Custom text test' ) ); + $this->setup_sidebar( 'sidebar-1', array( 'name' => 'Test sidebar' ), array( 'text-1', 'rss-1' ) ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/widgets/text-1' ); + $request->set_body_params( + array( + 'id' => 'text-1', + 'sidebar' => 'sidebar-1', + 'instance' => array( + 'raw' => array( + 'text' => 'Updated \\" \\\' text test', + ), + ), + 'id_base' => 'text', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEqualSets( + array( + 'text' => 'Updated \\" \\\' text test', + 'title' => '', + 'filter' => false, + ), + $data['instance']['raw'] + ); + + $this->assertEquals( + '
Updated \\" \\\' text test
', + $data['rendered'] + ); + } + + /** + * @ticket 41683 + */ + public function test_delete_item() { + $this->setup_widget( + 'text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( 'text-1', 'rss-1' ) + ); + + $request = new WP_REST_Request( 'DELETE', '/wp/v2/widgets/text-1' ); + $response = rest_do_request( $request ); + + $this->assertEqualSets( + array( + 'id' => 'text-1', + 'sidebar' => 'wp_inactive_widgets', + 'instance' => array( + 'encoded' => base64_encode( + serialize( + array( + 'text' => 'Custom text test', + ) + ) + ), + 'hash' => wp_hash( + serialize( + array( + 'text' => 'Custom text test', + ) + ) + ), + 'raw' => array( + 'text' => 'Custom text test', + ), + ), + 'id_base' => 'text', + 'rendered' => '', + 'rendered_form' => '' . "\n" . + ' ' . "\n" . + ' ' . "\n" . + ' ', + ), + $response->get_data() + ); + } + + /** + * @ticket 41683 + */ + public function test_delete_item_force() { + $this->setup_widget( + 'text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( 'text-1', 'rss-1' ) + ); + + $request = new WP_REST_Request( 'DELETE', '/wp/v2/widgets/text-1' ); + $request->set_query_params( array( 'force' => true ) ); + $response = rest_do_request( $request ); + + $this->assertEqualSets( + array( + 'deleted' => true, + 'previous' => array( + + 'id' => 'text-1', + 'sidebar' => 'sidebar-1', + 'instance' => array( + 'encoded' => base64_encode( + serialize( + array( + 'text' => 'Custom text test', + ) + ) + ), + 'hash' => wp_hash( + serialize( + array( + 'text' => 'Custom text test', + ) + ) + ), + 'raw' => array( + 'text' => 'Custom text test', + ), + ), + 'id_base' => 'text', + 'rendered' => '
Custom text test
', + 'rendered_form' => '' . "\n" . + ' ' . "\n" . + ' ' . "\n" . + ' ', + + ), + ), + $response->get_data() + ); + + $response = rest_do_request( '/wp/v2/widgets/text-1' ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * @ticket 41683 + */ + public function test_delete_item_logged_out() { + wp_set_current_user( 0 ); + + $this->setup_widget( + 'text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( 'text-1', 'rss-1' ) + ); + + $request = new WP_REST_Request( 'DELETE', '/wp/v2/widgets/text-1' ); + $response = rest_do_request( $request ); + + $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); + } + + /** + * @ticket 41683 + */ + public function test_delete_item_author() { + wp_set_current_user( self::$author_id ); + + $this->setup_widget( + 'text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( 'text-1', 'rss-1' ) + ); + + $request = new WP_REST_Request( 'DELETE', '/wp/v2/widgets/text-1' ); + $response = rest_do_request( $request ); + + $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 403 ); + } + + /** + * The test_prepare_item() method does not exist for sidebar. + */ + public function test_prepare_item() { + } + + /** + * @ticket 41683 + */ + public function test_get_item_schema() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/widgets' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 7, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'id_base', $properties ); + $this->assertArrayHasKey( 'sidebar', $properties ); + $this->assertArrayHasKey( 'rendered', $properties ); + $this->assertArrayHasKey( 'rendered_form', $properties ); + $this->assertArrayHasKey( 'instance', $properties ); + $this->assertArrayHasKey( 'form_data', $properties ); + } + + /** + * Helper to remove links key. + * + * @param array $data Array of data. + * + * @return array + */ + protected function remove_links( $data ) { + if ( ! is_array( $data ) ) { + return $data; + } + $count = 0; + foreach ( $data as $item ) { + if ( is_array( $item ) && isset( $item['_links'] ) ) { + unset( $data[ $count ]['_links'] ); + } + $count ++; + } + + return $data; + } +} diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 44cbd9040565c..dd8c11ddb5f7b 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -15,7 +15,8 @@ mockedApiResponse.Schema = { "namespaces": [ "oembed/1.0", "wp/v2", - "wp-site-health/v1" + "wp-site-health/v1", + "v1" ], "authentication": [], "routes": { @@ -6623,6 +6624,496 @@ mockedApiResponse.Schema = { } ] } + }, + "/v1": { + "namespace": "v1", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "namespace": { + "default": "v1", + "required": false + }, + "context": { + "default": "view", + "required": false + } + } + } + ], + "_links": { + "self": [ + { + "href": "http://example.org/index.php?rest_route=/v1" + } + ] + } + }, + "/v1/batch": { + "namespace": "v1", + "methods": [ + "POST" + ], + "endpoints": [ + { + "methods": [ + "POST" + ], + "args": { + "validation": { + "type": "string", + "enum": [ + "require-all-validate", + "normal" + ], + "default": "normal", + "required": false + }, + "requests": { + "type": "array", + "maxItems": 25, + "items": { + "type": "object", + "properties": { + "method": { + "type": "string", + "enum": [ + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "default": "POST" + }, + "path": { + "type": "string", + "required": true + }, + "body": { + "type": "object", + "properties": [], + "additionalProperties": true + }, + "headers": { + "type": "object", + "properties": [], + "additionalProperties": { + "type": [ + "string", + "array" + ], + "items": { + "type": "string" + } + } + } + } + }, + "required": true + } + } + } + ], + "_links": { + "self": [ + { + "href": "http://example.org/index.php?rest_route=/v1/batch" + } + ] + } + }, + "/wp/v2/sidebars": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + } + } + } + ], + "_links": { + "self": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/sidebars" + } + ] + } + }, + "/wp/v2/sidebars/(?P[\\w-]+)": { + "namespace": "wp/v2", + "methods": [ + "GET", + "POST", + "PUT", + "PATCH" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "id": { + "description": "The id of a registered sidebar", + "type": "string", + "required": false + }, + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + } + } + }, + { + "methods": [ + "POST", + "PUT", + "PATCH" + ], + "args": { + "widgets": { + "description": "Nested widgets.", + "type": "array", + "items": { + "type": [ + "object", + "string" + ] + }, + "required": false + } + } + } + ] + }, + "/wp/v2/widget-types": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + } + } + } + ], + "_links": { + "self": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/widget-types" + } + ] + } + }, + "/wp/v2/widget-types/(?P[a-zA-Z0-9_-]+)": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "id": { + "description": "The widget type id.", + "type": "string", + "required": false + }, + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + } + } + } + ] + }, + "/wp/v2/widget-types/(?P[a-zA-Z0-9_-]+)/encode": { + "namespace": "wp/v2", + "methods": [ + "POST" + ], + "endpoints": [ + { + "methods": [ + "POST" + ], + "args": { + "id": { + "description": "The widget type id.", + "type": "string", + "required": true + }, + "instance": { + "description": "Current instance settings of the widget.", + "type": "object", + "required": false + }, + "form_data": { + "description": "Serialized widget form data to encode into instance settings.", + "type": "string", + "required": false + } + } + } + ] + }, + "/wp/v2/widgets": { + "namespace": "wp/v2", + "methods": [ + "GET", + "POST" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + }, + "sidebar": { + "description": "The sidebar to return widgets for.", + "type": "string", + "required": false + } + } + }, + { + "methods": [ + "POST" + ], + "args": { + "id": { + "description": "Unique identifier for the widget.", + "type": "string", + "required": false + }, + "id_base": { + "description": "The type of the widget. Corresponds to ID in widget-types endpoint.", + "type": "string", + "required": false + }, + "sidebar": { + "default": "wp_inactive_widgets", + "description": "The sidebar the widget belongs to.", + "type": "string", + "required": true + }, + "instance": { + "description": "Instance settings of the widget, if supported.", + "type": "object", + "properties": { + "encoded": { + "description": "Base64 encoded representation of the instance settings.", + "type": "string", + "context": [ + "view", + "edit", + "embed" + ] + }, + "hash": { + "description": "Cryptographic hash of the instance settings.", + "type": "string", + "context": [ + "view", + "edit", + "embed" + ] + }, + "raw": { + "description": "Unencoded instance settings, if supported.", + "type": "object", + "context": [ + "view", + "edit", + "embed" + ] + } + }, + "required": false + }, + "form_data": { + "description": "URL-encoded form data from the widget admin form. Used to update a widget that does not support instance. Write only.", + "type": "string", + "required": false + } + } + } + ], + "_links": { + "self": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/widgets" + } + ] + } + }, + "/wp/v2/widgets/(?P[\\w\\-]+)": { + "namespace": "wp/v2", + "methods": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + } + } + }, + { + "methods": [ + "POST", + "PUT", + "PATCH" + ], + "args": { + "id": { + "description": "Unique identifier for the widget.", + "type": "string", + "required": false + }, + "id_base": { + "description": "The type of the widget. Corresponds to ID in widget-types endpoint.", + "type": "string", + "required": false + }, + "sidebar": { + "description": "The sidebar the widget belongs to.", + "type": "string", + "required": false + }, + "instance": { + "description": "Instance settings of the widget, if supported.", + "type": "object", + "properties": { + "encoded": { + "description": "Base64 encoded representation of the instance settings.", + "type": "string", + "context": [ + "view", + "edit", + "embed" + ] + }, + "hash": { + "description": "Cryptographic hash of the instance settings.", + "type": "string", + "context": [ + "view", + "edit", + "embed" + ] + }, + "raw": { + "description": "Unencoded instance settings, if supported.", + "type": "object", + "context": [ + "view", + "edit", + "embed" + ] + } + }, + "required": false + }, + "form_data": { + "description": "URL-encoded form data from the widget admin form. Used to update a widget that does not support instance. Write only.", + "type": "string", + "required": false + } + } + }, + { + "methods": [ + "DELETE" + ], + "args": { + "force": { + "description": "Whether to force removal of the widget, or move it to the inactive sidebar.", + "type": "boolean", + "required": false + } + } + } + ] } } };