diff --git a/lib/class-wp-rest-sidebars-controller.php b/lib/class-wp-rest-sidebars-controller.php index 16b827e70ad178..17381f07139488 100644 --- a/lib/class-wp-rest-sidebars-controller.php +++ b/lib/class-wp-rest-sidebars-controller.php @@ -34,12 +34,12 @@ class WP_REST_Sidebars_Controller extends WP_REST_Controller { /** - * Plugins controller constructor. + * Sidebars controller constructor. * * @since 5.5.0 */ public function __construct() { - $this->namespace = '__experimental'; + $this->namespace = 'wp/v2'; $this->rest_base = 'sidebars'; } @@ -58,6 +58,9 @@ public function register_routes() { 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) @@ -73,13 +76,11 @@ public function register_routes() { 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'permissions_check' ), 'args' => array( - 'id' => array( - 'description' => __( 'The id of a registered sidebar', 'gutenberg' ), - 'type' => 'string', - 'validate_callback' => function ( $id ) { - return self::get_sidebar( $id )[0]; - }, + 'id' => array( + 'description' => __( 'The id of a registered sidebar', 'gutenberg' ), + 'type' => 'string', ), + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), array( @@ -91,13 +92,148 @@ public function register_routes() { 'schema' => array( $this, 'get_public_item_schema' ), ) ); + + // Lists all sidebars. + register_rest_route( + '__experimental', + '/' . $this->rest_base, + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'do_items_shim' ), + 'permission_callback' => '__return_true', + ) + ); + + // Lists/updates a single sidebar based on the given id. + register_rest_route( + '__experimental', + '/' . $this->rest_base . '/(?P[\w-]+)', + array( + 'methods' => WP_REST_Server::ALLMETHODS, + 'callback' => array( $this, 'do_item_shim' ), + 'permission_callback' => '__return_true', + ) + ); + } + + /** + * Shims the items route to use the new endpoint. + * + * @param WP_REST_Request $request The full details about the request. + * + * @return WP_REST_Response The computed response. + */ + public function do_items_shim( $request ) { + $inner = new WP_REST_Request( + $request->get_method(), + str_replace( '/__experimental/', '/wp/v2/', $request->get_route() ) + ); + $inner->set_query_params( $request->get_query_params() ); + + $response = rest_do_request( $inner ); + + if ( $response->is_error() ) { + return $response; + } + + foreach ( $response->get_data() as $i => $data ) { + foreach ( $data['widgets'] as $j => $widget ) { + $response->data[ $i ]['widgets'][ $j ] = rest_do_request( '/wp/v2/widgets/' . $widget )->get_data(); + } + + $response->data[ $i ]['widgets'] = array_values( $response->data[ $i ]['widgets'] ); + } + + return $response; + } + + /** + * Shims the item route to use the new endpoint. + * + * @param WP_REST_Request $request The full details about the request. + * + * @return WP_REST_Response The computed response. + */ + public function do_item_shim( $request ) { + if ( $request->get_method() === 'GET' ) { + $inner = new WP_REST_Request( + $request->get_method(), + str_replace( '/__experimental/', '/wp/v2/', $request->get_route() ) + ); + $inner->set_query_params( $request->get_query_params() ); + + $response = rest_do_request( $inner ); + + if ( $response->is_error() ) { + return $response; + } + + foreach ( $response->data['widgets'] as $i => $widget ) { + $response->data['widgets'][ $i ] = rest_do_request( '/wp/v2/widgets/' . $widget )->get_data(); + } + + return $response; + } + + $widgets = null; + + if ( isset( $request['widgets'] ) ) { + $widgets = array(); + + foreach ( $request['widgets'] as $widget ) { + if ( isset( $widget['id'] ) ) { + $widget_request = new WP_REST_Request( 'PUT', '/wp/v2/widgets/' . $widget['id'] ); + } else { + $widget_request = new WP_REST_Request( 'POST', '/wp/v2/widgets' ); + } + + $widget['sidebar'] = $request['id']; + $widget_request->set_body_params( $widget ); + + $response = rest_do_request( $widget_request ); + + if ( is_wp_error( $response->as_error() ) ) { + return $response->as_error(); + } + + $widgets[] = $response->get_data(); + } + } + + $inner = new WP_REST_Request( + $request->get_method(), + str_replace( '/__experimental/', '/wp/v2/', $request->get_route() ) + ); + $inner->set_query_params( $request->get_query_params() ); + $inner->set_body_params( $request->get_body_params() ); + $inner->set_body( $request->get_body() ); + + if ( null !== $widgets ) { + $inner['widgets'] = wp_list_pluck( $widgets, 'id' ); + } + + $response = rest_do_request( $inner ); + + if ( $response->is_error() ) { + return $response; + } + + if ( isset( $response->data['widgets'] ) ) { + foreach ( $response->data['widgets'] as $i => $widget ) { + $response->data['widgets'][ $i ] = $widgets[ $i ]; + } + + $response->data['widgets'] = array_values( $response->data['widgets'] ); + } + + return $response; } /** * Checks if the user has permissions to make the request. * * @return true|WP_Error True if the request has read access, WP_Error object otherwise. - * @since 5.6.0 + * @since 5.6.0 * @access public */ public function permissions_check() { @@ -116,128 +252,75 @@ public function permissions_check() { return true; } - /** - * Updates the sidebar. + * Returns a list of sidebars (active or inactive) * * @param WP_REST_Request $request The request instance. * * @return WP_REST_Response - * @global array $wp_registered_widget_updates */ - public function update_item( $request ) { - global $wp_registered_widget_updates, $wp_registered_widgets; - $sidebar_id = $request['id']; - $input_widgets = $request['widgets']; - - // Initialize $numbers. - $numbers = array(); - foreach ( $wp_registered_widget_updates as $id_base => $control ) { - if ( is_array( $control['callback'] ) ) { - $numbers[ $id_base ] = $control['callback'][0]->number + 1; - } - } - - // Create and update widgets. - $sidebar_widgets_ids = array(); - foreach ( $input_widgets as $input_widget ) { - ob_start(); - if ( isset( $input_widget['id_base'] ) && isset( $wp_registered_widget_updates[ $input_widget['id_base'] ] ) ) { - // Class-based widget. - $update_control = $wp_registered_widget_updates[ $input_widget['id_base'] ]; - if ( ! isset( $input_widget['id'] ) ) { - $number = $numbers[ $input_widget['id_base'] ] ++; - $id = $input_widget['id_base'] . '-' . $number; - - $input_widget['id'] = $id; - $input_widget['number'] = $number; - } - $field = 'widget-' . $input_widget['id_base']; - $number = $input_widget['number']; - $_POST = $input_widget; - $_POST[ $field ][ $number ] = wp_slash( $input_widget['settings'] ); - call_user_func( $update_control['callback'] ); - $update_control['callback'][0]->updated = false; - - // Just because we saved new widget doesn't mean it was added to $wp_registered_widgets. - // Let's make sure it's there so that it's included in the response. - if ( ! isset( $wp_registered_widgets[ $input_widget['id'] ] ) ) { - $first_widget_id = substr( $input_widget['id'], 0, strrpos( $input_widget['id'], '-' ) ) . '-1'; - - if ( isset( $wp_registered_widgets[ $first_widget_id ] ) ) { - $wp_registered_widgets[ $input_widget['id'] ] = $wp_registered_widgets[ $first_widget_id ]; - $widget_class = get_class( $update_control['callback'][0] ); - $new_object = new $widget_class( - $input_widget['id_base'], - $input_widget['name'], - $input_widget['settings'] - ); - $new_object->_register(); - $wp_registered_widgets[ $input_widget['id'] ]['callback'][0] = $new_object; - } - } - } else { - $registered_widget_id = null; - if ( isset( $wp_registered_widget_updates[ $input_widget['id'] ] ) ) { - $registered_widget_id = $input_widget['id']; - } else { - $numberless_id = substr( $input_widget['id'], 0, strrpos( $input_widget['id'], '-' ) ); - if ( isset( $wp_registered_widget_updates[ $numberless_id ] ) ) { - $registered_widget_id = $numberless_id; - } - } + public function get_items( $request ) { + $data = array(); + foreach ( (array) wp_get_sidebars_widgets() as $id => $widgets ) { + list( $exists, $sidebar ) = $this->get_sidebar( $id ); - if ( $registered_widget_id ) { - // Old-style widget. - $update_control = $wp_registered_widget_updates[ $registered_widget_id ]; - $_POST = wp_slash( $input_widget['settings'] ); - call_user_func( $update_control['callback'] ); - } + if ( ! $exists && 'wp_inactive_widgets' !== $id ) { + continue; } - ob_end_clean(); - $sidebar_widgets_ids[] = $input_widget['id']; + $data[] = $this->prepare_response_for_collection( + $this->prepare_item_for_response( $sidebar, $request ) + ); } - // Update sidebar to only consist of the widgets we just processed. - $sidebars = wp_get_sidebars_widgets(); - $sidebars[ $sidebar_id ] = $sidebar_widgets_ids; - wp_set_sidebars_widgets( $sidebars ); - - $request = new WP_REST_Request( 'GET' ); - $request->set_param( 'id', $sidebar_id ); - - return $this->get_item( $request ); + return rest_ensure_response( $data ); } /** - * Returns a list of sidebars (active or inactive) + * Returns the given sidebar * * @param WP_REST_Request $request The request instance. * - * @return WP_REST_Response - * @global array $wp_registered_sidebars + * @return WP_REST_Response|WP_Error */ - public function get_items( $request ) { - $data = array(); - foreach ( (array) wp_get_sidebars_widgets() as $id => $widgets ) { - $sidebar = self::get_sidebar( $id )[1]; + public function get_item( $request ) { + list( $exists, $sidebar ) = $this->get_sidebar( $request['id'] ); - $data[] = $this->prepare_item_for_response( $sidebar, $request )->get_data(); + if ( ! $exists && 'wp_inactive_widgets' !== $request['id'] ) { + return new WP_Error( 'rest_sidebar_not_found', __( 'No sidebar exists with that id.', 'gutenberg' ), array( 'status' => 404 ) ); } - return rest_ensure_response( $data ); + return $this->prepare_item_for_response( $sidebar, $request ); } /** - * Returns the given sidebar + * Updates the sidebar. * * @param WP_REST_Request $request The request instance. * * @return WP_REST_Response */ - public function get_item( $request ) { - $sidebar = self::get_sidebar( $request['id'] )[1]; + 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 ) { + if ( $sidebar_id !== $request['id'] && in_array( $widget_id, $request['widgets'], true ) ) { + unset( $sidebars[ $sidebar_id ][ $i ] ); + $sidebars['wp_inactive_widgets'][] = $widget_id; + } + } + } + + $sidebars[ $request['id'] ] = $request['widgets']; + + wp_set_sidebars_widgets( $sidebars ); + } + + $request['context'] = 'edit'; + + list( , $sidebar ) = $this->get_sidebar( $request['id'] ); return $this->prepare_item_for_response( $sidebar, $request ); } @@ -250,9 +333,9 @@ public function get_item( $request ) { * @param string|int $id ID of the sidebar. * * @return array|null - * @global array $wp_registered_sidebars + * @global array $wp_registered_sidebars */ - public static function get_sidebar( $id ) { + protected function get_sidebar( $id ) { global $wp_registered_sidebars; if ( is_int( $id ) ) { @@ -276,114 +359,16 @@ public static function get_sidebar( $id ) { return array( false, array( 'id' => $id ) ); } - /** - * Returns a list of widgets for the given sidebar id - * - * @param string $sidebar_id ID of the sidebar. - * @param WP_REST_Request $request Request object. - * - * @return array - * @global array $wp_registered_widgets - * @global array $wp_registered_sidebars - */ - public static function get_widgets( $sidebar_id, $request ) { - global $wp_registered_widgets, $wp_registered_sidebars, $wp_registered_widget_controls; - - $widgets = array(); - $sidebars_widgets = (array) wp_get_sidebars_widgets(); - $registered_sidebar = isset( $wp_registered_sidebars[ $sidebar_id ] ) - ? $wp_registered_sidebars[ $sidebar_id ] - : ( - 'wp_inactive_widgets' === $sidebar_id ? array() : null - ); - - if ( null !== $registered_sidebar && isset( $sidebars_widgets[ $sidebar_id ] ) ) { - foreach ( $sidebars_widgets[ $sidebar_id ] as $widget_id ) { - // Just to be sure. - if ( isset( $wp_registered_widgets[ $widget_id ] ) ) { - $widget = $wp_registered_widgets[ $widget_id ]; - - // Get the widget output. - if ( is_callable( $widget['callback'] ) ) { - // @note: everything up to ob_start is taken from the dynamic_sidebar function. - $widget_parameters = array_merge( - array( - array_merge( - $registered_sidebar, - array( - 'widget_id' => $widget_id, - 'widget_name' => $widget['name'], - ) - ), - ), - (array) $widget['params'] - ); - - $classname = ''; - foreach ( (array) $widget['classname'] as $cn ) { - if ( is_string( $cn ) ) { - $classname .= '_' . $cn; - } elseif ( is_object( $cn ) ) { - $classname .= '_' . get_class( $cn ); - } - } - $classname = ltrim( $classname, '_' ); - if ( isset( $widget_parameters[0]['before_widget'] ) ) { - $widget_parameters[0]['before_widget'] = sprintf( - $widget_parameters[0]['before_widget'], - $widget_id, - $classname - ); - } - - ob_start(); - call_user_func_array( $widget['callback'], $widget_parameters ); - $widget['rendered'] = trim( ob_get_clean() ); - } - - if ( is_array( $widget['callback'] ) && isset( $widget['callback'][0] ) ) { - $instance = $widget['callback'][0]; - $widget['widget_class'] = get_class( $instance ); - $widget['settings'] = static::get_sidebar_widget_instance( - $registered_sidebar, - $widget_id - ); - $widget['number'] = (int) $widget['params'][0]['number']; - $widget['id_base'] = $instance->id_base; - } - - if ( 'edit' === $request['context'] && isset( $wp_registered_widget_controls[ $widget_id ]['callback'] ) ) { - $control = $wp_registered_widget_controls[ $widget_id ]; - $arguments = array(); - if ( ! empty( $widget['number'] ) ) { - $arguments[0] = array( 'number' => $widget['number'] ); - } - ob_start(); - call_user_func_array( $control['callback'], $arguments ); - $widget['rendered_form'] = trim( ob_get_clean() ); - } - - unset( $widget['params'] ); - unset( $widget['callback'] ); - - $widgets[] = $widget; - } - } - } - - return $widgets; - } - /** * Prepare a single sidebar output for response * * @param array $raw_sidebar Sidebar instance. - * @param WP_REST_Request $request Request object. + * @param WP_REST_Request $request Request object. * * @return WP_REST_Response $data */ public function prepare_item_for_response( $raw_sidebar, $request ) { - global $wp_registered_sidebars; + global $wp_registered_sidebars, $wp_registered_widgets; $id = $raw_sidebar['id']; $sidebar = array( 'id' => $id ); @@ -391,9 +376,14 @@ public function prepare_item_for_response( $raw_sidebar, $request ) { 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['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'; } @@ -404,35 +394,35 @@ public function prepare_item_for_response( $raw_sidebar, $request ) { $fields = $this->get_fields_for_response( $request ); if ( rest_is_field_included( 'widgets', $fields ) ) { - $sidebar['widgets'] = self::get_widgets( $sidebar['id'], $request ); + $sidebars = wp_get_sidebars_widgets(); + $widgets = array_filter( + isset( $sidebars[ $sidebar['id'] ] ) ? $sidebars[ $sidebar['id'] ] : array(), + 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 ] ) && gettype( $sidebar[ $property_id ] ) === $property['type'] ) { + 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']; } } - foreach ( $sidebar['widgets'] as $widget_id => $widget ) { - $widget_data = array(); - foreach ( $schema['properties']['widgets']['items']['properties'] as $property_id => $property ) { - if ( isset( $widget[ $property_id ] ) && gettype( $widget[ $property_id ] ) === $property['type'] ) { - $widget_data[ $property_id ] = $widget[ $property_id ]; - } elseif ( 'settings' === $property_id && isset( $widget[ $property_id ] ) && 'array' === gettype( $widget[ $property_id ] ) ) { - $widget_data[ $property_id ] = $widget['settings']; - } elseif ( isset( $property['default'] ) ) { - $widget_data[ $property_id ] = $property['default']; - } - } - $data['widgets'][ $widget_id ] = $widget_data; - } + $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 a sidebar location returned from the REST API. * @@ -440,12 +430,34 @@ public function prepare_item_for_response( $raw_sidebar, $request ) { * returned. * * @param WP_REST_Response $response The response object. - * @param object $sidebar The original status object. - * @param WP_REST_Request $request Request used to generate the response. + * @param object $sidebar The original status object. + * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( 'rest_prepare_sidebar', $response, $sidebar, $request ); } + /** + * Prepares links for the request. + * + * @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( sprintf( '%s/%s', 'wp/v2', 'widgets' ) ) ), + 'embeddable' => true, + ), + ); + } + /** * Retrieves the block type' schema, conforming to JSON Schema. * @@ -461,28 +473,63 @@ public function get_item_schema() { 'title' => 'sidebar', 'type' => 'object', 'properties' => array( - 'id' => array( + 'id' => array( 'description' => __( 'ID of sidebar.', 'gutenberg' ), 'type' => 'string', 'default' => '', 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'name' => array( + 'name' => array( 'description' => __( 'Unique name identifying the sidebar.', 'gutenberg' ), 'type' => 'string', 'default' => '', 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'description' => array( + 'description' => array( 'description' => __( 'Description of sidebar.', 'gutenberg' ), 'type' => 'string', 'default' => '', 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'status' => array( + 'class' => array( + 'description' => __( 'Extra CSS class to assign to the sidebar in the Widgets interface.', 'gutenberg' ), + 'type' => 'string', + 'default' => '', + '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.', 'gutenberg' ), + '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.', 'gutenberg' ), + '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.', 'gutenberg' ), + '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.', 'gutenberg' ), + 'type' => 'string', + 'default' => '', + 'context' => array( 'embed', 'view', 'edit' ), + 'readonly' => true, + ), + 'status' => array( 'description' => __( 'Status of sidebar.', 'gutenberg' ), 'type' => 'string', 'enum' => array( 'active', 'inactive' ), @@ -490,61 +537,11 @@ public function get_item_schema() { 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'widgets' => array( + 'widgets' => array( 'description' => __( 'Nested widgets.', 'gutenberg' ), 'type' => 'array', 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Unique identifier for the widget.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'id_base' => array( - 'description' => __( 'Type of widget for the object.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'widget_class' => array( - 'description' => __( 'Class name of the widget implementation.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'name' => array( - 'description' => __( 'Name of the widget.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'description' => array( - 'description' => __( 'Description of the widget.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'number' => array( - 'description' => __( 'Number of the widget.', 'gutenberg' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'rendered' => array( - 'description' => __( 'HTML representation of the widget.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'view', 'embed' ), - 'readonly' => true, - ), - 'rendered_form' => array( - 'description' => __( 'HTML representation of the widget admin form.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'edit' ), - 'readonly' => true, - ), - 'settings' => array( - 'description' => __( 'Settings of the widget.', 'gutenberg' ), - 'type' => 'object', - 'context' => array( 'view', 'edit', 'embed' ), - 'default' => array(), - ), - ), + 'type' => array( 'object', 'string' ), ), 'default' => array(), 'context' => array( 'embed', 'view', 'edit' ), @@ -556,80 +553,4 @@ public function get_item_schema() { return $this->add_additional_fields_schema( $this->schema ); } - - /** - * Retrieves a widget instance. - * - * @param array $sidebar sidebar data available at $wp_registered_sidebars. - * @param string $id Identifier of the widget instance. - * - * @return array Array containing the widget instance. - * @since 5.7.0 - */ - public static function get_sidebar_widget_instance( $sidebar, $id ) { - list( $object, $number, $name ) = static::get_widget_info( $id ); - if ( ! $object ) { - return array(); - } - - $object->_set( $number ); - - $instances = $object->get_settings(); - $instance = $instances[ $number ]; - - $args = array_merge( - $sidebar, - array( - 'widget_id' => $id, - 'widget_name' => $name, - ) - ); - - /** - * Filters the settings for a particular widget instance. - * - * Returning false will effectively short-circuit display of the widget. - * - * @param array $instance The current widget instance's settings. - * @param WP_Widget $this The current widget instance. - * @param array $args An array of default widget arguments. - * - * @since 2.8.0 - */ - $instance = apply_filters( 'widget_display_callback', $instance, $object, $args ); - - if ( false === $instance ) { - return array(); - } - - return $instance; - } - - /** - * Given a widget id returns an array containing information about the widget. - * - * @param string $widget_id Identifier of the widget. - * - * @return array Array containing the the widget object, the number, and the name. - * @since 5.7.0 - */ - private static function get_widget_info( $widget_id ) { - global $wp_registered_widgets; - - if ( - ! isset( $wp_registered_widgets[ $widget_id ]['callback'][0] ) || - ! isset( $wp_registered_widgets[ $widget_id ]['params'][0]['number'] ) || - ! isset( $wp_registered_widgets[ $widget_id ]['name'] ) || - ! ( $wp_registered_widgets[ $widget_id ]['callback'][0] instanceof WP_Widget ) - ) { - return array( null, null, null ); - } - - $object = $wp_registered_widgets[ $widget_id ]['callback'][0]; - $number = $wp_registered_widgets[ $widget_id ]['params'][0]['number']; - $name = $wp_registered_widgets[ $widget_id ]['name']; - - return array( $object, $number, $name ); - } - } diff --git a/lib/class-wp-rest-widgets-controller.php b/lib/class-wp-rest-widgets-controller.php new file mode 100644 index 00000000000000..c320e410177798 --- /dev/null +++ b/lib/class-wp-rest-widgets-controller.php @@ -0,0 +1,771 @@ +namespace = 'wp/v2'; + $this->rest_base = 'widgets'; + } + + /** + * Registers the widget routes for the controller. + */ + 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' => 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.', 'gutenberg' ), + 'type' => 'boolean', + ), + ), + ), + 'allow_batch' => true, + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Checks if a given request has access to get widgets. + * + * @since 5.6.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. + */ + public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + return $this->permissions_check(); + } + + /** + * Retrieves a collection of widgets. + * + * @since 5.6.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.6.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 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + return $this->permissions_check(); + } + + /** + * Gets an individual widget. + * + * @since 5.6.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 = $this->find_widgets_sidebar( $widget_id ); + + if ( is_wp_error( $sidebar_id ) ) { + return $sidebar_id; + } + + return $this->prepare_item_for_response( compact( 'sidebar_id', 'widget_id' ), $request ); + } + + /** + * Checks if a given request has access to create widgets. + * + * @since 5.6.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 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + return $this->permissions_check(); + } + + /** + * Creates a widget. + * + * @since 5.6.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']; + + $backup_post = $_POST; + $widget_id = $this->save_widget( $request ); + $_POST = $backup_post; + + $this->assign_to_sidebar( $widget_id, $sidebar_id ); + + $request['context'] = 'edit'; + + $response = $this->prepare_item_for_response( compact( 'sidebar_id', 'widget_id' ), $request ); + $response->set_status( 201 ); + + return $response; + } + + /** + * Checks if a given request has access to update widgets. + * + * @since 5.6.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 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + return $this->permissions_check(); + } + + /** + * Updates an existing widget. + * + * @since 5.6.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 ) { + $widget_id = $request['id']; + $sidebar_id = $this->find_widgets_sidebar( $widget_id ); + + if ( is_wp_error( $sidebar_id ) ) { + // Allow for an update request to a reference widget if the widget hasn't been assigned to a sidebar yet. + if ( $request['sidebar'] && $this->is_reference_widget( $widget_id ) ) { + $sidebar_id = $request['sidebar']; + $this->assign_to_sidebar( $widget_id, $sidebar_id ); + } else { + return $sidebar_id; + } + } + + $backup_post = $_POST; + + if ( isset( $request['settings'] ) ) { + $this->save_widget( $request ); + } + + $_POST = $backup_post; + + if ( isset( $request['sidebar'] ) && $request['sidebar'] !== $sidebar_id ) { + $sidebar_id = $request['sidebar']; + $this->assign_to_sidebar( $widget_id, $sidebar_id ); + } + + $request['context'] = 'edit'; + + return $this->prepare_item_for_response( compact( 'sidebar_id', 'widget_id' ), $request ); + } + + /** + * Checks if a given request has access to delete widgets. + * + * @since 5.6.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 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + return $this->permissions_check(); + } + + /** + * Deletes a widget. + * + * @since 5.6.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 = $this->find_widgets_sidebar( $widget_id ); + + if ( is_wp_error( $sidebar_id ) ) { + return $sidebar_id; + } + + $request['context'] = 'edit'; + + if ( $request['force'] ) { + $prepared = $this->prepare_item_for_response( compact( 'sidebar_id', 'widget_id' ), $request ); + $this->assign_to_sidebar( $widget_id, null ); + $prepared->set_data( + array( + 'deleted' => true, + 'previous' => $prepared->get_data(), + ) + ); + } else { + $this->assign_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; + } + + /** + * Assigns a widget to the given sidebar. + * + * @param string $widget_id The widget id to assign. + * @param string $sidebar_id The sidebar id to assign to. + */ + protected function assign_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 ); + } + + /** + * Performs a permissions check for managing widgets. + * + * @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.', 'gutenberg' ), + array( + 'status' => rest_authorization_required_code(), + ) + ); + } + + return true; + } + + /** + * Finds the sidebar a widget belongs to. + * + * @param string $widget_id The widget id to search for. + * @return string|WP_Error The found sidebar id, or a WP_Error instance if it does not exist. + */ + protected function 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 new WP_Error( 'rest_widget_not_found', __( 'No widget was found with that id.', 'gutenberg' ), array( 'status' => 404 ) ); + } + + /** + * Saves the widget in the request object. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return string + */ + protected function save_widget( $request ) { + global $wp_registered_widget_updates, $wp_registered_widgets; + + $input_widget = $request->get_params(); + + ob_start(); + if ( isset( $input_widget['id_base'] ) && isset( $wp_registered_widget_updates[ $input_widget['id_base'] ] ) ) { + // Class-based widget. + $update_control = $wp_registered_widget_updates[ $input_widget['id_base'] ]; + if ( ! isset( $input_widget['id'] ) ) { + $number = $this->get_last_number_for_widget( $input_widget['id_base'] ) + 1; + $id = $input_widget['id_base'] . '-' . $number; + + $input_widget['id'] = $id; + $input_widget['number'] = $number; + } + $field = 'widget-' . $input_widget['id_base']; + $number = $input_widget['number']; + $_POST = $input_widget; + $_POST[ $field ][ $number ] = wp_slash( $input_widget['settings'] ); + call_user_func( $update_control['callback'] ); + $update_control['callback'][0]->updated = false; + + // Just because we saved new widget doesn't mean it was added to $wp_registered_widgets. + // Let's make sure it's there so that it's included in the response. + if ( ! isset( $wp_registered_widgets[ $input_widget['id'] ] ) || 1 === $number ) { + $first_widget_id = substr( $input_widget['id'], 0, strrpos( $input_widget['id'], '-' ) ) . '-1'; + + if ( isset( $wp_registered_widgets[ $first_widget_id ] ) ) { + $wp_registered_widgets[ $input_widget['id'] ] = $wp_registered_widgets[ $first_widget_id ]; + + $widget_class = get_class( $update_control['callback'][0] ); + $new_object = new $widget_class( + $input_widget['id_base'], + $input_widget['name'], + $input_widget['settings'] + ); + $new_object->_register(); + $wp_registered_widgets[ $input_widget['id'] ]['callback'][0] = $new_object; + } + } + } else { + $registered_widget_id = null; + if ( isset( $wp_registered_widget_updates[ $input_widget['id'] ] ) ) { + $registered_widget_id = $input_widget['id']; + } else { + $numberless_id = substr( $input_widget['id'], 0, strrpos( $input_widget['id'], '-' ) ); + if ( isset( $wp_registered_widget_updates[ $numberless_id ] ) ) { + $registered_widget_id = $numberless_id; + } + } + + if ( $registered_widget_id ) { + // Old-style widget. + $update_control = $wp_registered_widget_updates[ $registered_widget_id ]; + $_POST = wp_slash( $input_widget['settings'] ); + call_user_func( $update_control['callback'] ); + } + } + ob_end_clean(); + + return $input_widget['id']; + } + + /** + * Gets the last number used by the given widget. + * + * @param string $id_base The widget id base. + * @return int The last number, or zero if the widget has not been used. + */ + protected function get_last_number_for_widget( $id_base ) { + global $wp_registered_widget_updates; + + if ( ! is_array( $wp_registered_widget_updates[ $id_base ]['callback'] ) ) { + return 0; + } + + if ( ! $wp_registered_widget_updates[ $id_base ]['callback'][0] instanceof WP_Widget ) { + return 0; + } + + $widget = $wp_registered_widget_updates[ $id_base ]['callback'][0]; + $instances = array_filter( $widget->get_settings(), 'is_numeric', ARRAY_FILTER_USE_KEY ); + + if ( ! $instances ) { + return 0; + } + + return $widget->number; + } + + /** + * Prepares the widget for the REST response. + * + * @since 5.6.0 + * + * @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_registered_widgets, $wp_registered_sidebars, $wp_registered_widget_controls; + + $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.', 'gutenberg' ), array( 'status' => 500 ) ); + } + + $fields = $this->get_fields_for_response( $request ); + + if ( isset( $wp_registered_sidebars[ $sidebar_id ] ) ) { + $registered_sidebar = $wp_registered_sidebars[ $sidebar_id ]; + } elseif ( 'wp_inactive_widgets' === $sidebar_id ) { + $registered_sidebar = array(); + } else { + $registered_sidebar = null; + } + + $widget = $wp_registered_widgets[ $widget_id ]; + $prepared = array( + 'id' => $widget_id, + 'id_base' => '', + 'sidebar' => $sidebar_id, + 'widget_class' => '', + 'name' => $widget['name'], + 'description' => ! empty( $widget['description'] ) ? $widget['description'] : '', + 'number' => 0, + 'rendered' => '', + 'rendered_form' => '', + 'settings' => array(), + ); + + // Get the widget output. + if ( is_callable( $widget['callback'] ) && rest_is_field_included( 'rendered', $fields ) && 'wp_inactive_widgets' !== $sidebar_id ) { + // @note: everything up to ob_start is taken from the dynamic_sidebar function. + $widget_parameters = array_merge( + array( + array_merge( + (array) $registered_sidebar, + array( + 'widget_id' => $widget_id, + 'widget_name' => $widget['name'], + ) + ), + ), + (array) $widget['params'] + ); + + $classname = ''; + foreach ( (array) $widget['classname'] as $cn ) { + if ( is_string( $cn ) ) { + $classname .= '_' . $cn; + } elseif ( is_object( $cn ) ) { + $classname .= '_' . get_class( $cn ); + } + } + $classname = ltrim( $classname, '_' ); + if ( isset( $widget_parameters[0]['before_widget'] ) ) { + $widget_parameters[0]['before_widget'] = sprintf( + $widget_parameters[0]['before_widget'], + $widget_id, + $classname + ); + } + + ob_start(); + call_user_func_array( $widget['callback'], $widget_parameters ); + $prepared['rendered'] = trim( ob_get_clean() ); + } + + if ( is_array( $widget['callback'] ) && isset( $widget['callback'][0] ) ) { + $instance = $widget['callback'][0]; + $prepared['widget_class'] = get_class( $instance ); + $prepared['settings'] = $this->get_sidebar_widget_instance( + $registered_sidebar, + $widget_id + ); + $prepared['number'] = (int) $widget['params'][0]['number']; + $prepared['id_base'] = $instance->id_base; + } + + if ( + rest_is_field_included( 'rendered_form', $fields ) && + isset( $wp_registered_widget_controls[ $widget_id ]['callback'] ) + ) { + $control = $wp_registered_widget_controls[ $widget_id ]; + $arguments = array(); + if ( ! empty( $widget['number'] ) ) { + $arguments[0] = array( 'number' => $widget['number'] ); + } + ob_start(); + call_user_func_array( $control['callback'], $arguments ); + $prepared['rendered_form'] = trim( ob_get_clean() ); + } + + $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 ) ); + + /** + * Filter widget REST API response. + * + * @param WP_REST_Response $response The response object. + * @param array $prepared Widget data. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'rest_prepare_widget', $response, $prepared, $request ); + } + + /** + * Prepares links for the request. + * + * @param array $prepared Widget. + * @return array Links for the given widget. + */ + protected function prepare_links( $prepared ) { + 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, $prepared['id'] ) ), + ), + 'https://api.w.org/sidebar' => array( + 'href' => rest_url( sprintf( 'wp/v2/sidebars/%s/', $prepared['sidebar'] ) ), + ), + ); + } + + /** + * Retrieves a widget instance. + * + * @param array $sidebar sidebar data available at $wp_registered_sidebars. + * @param string $id Identifier of the widget instance. + * + * @return array Array containing the widget instance. + */ + protected function get_sidebar_widget_instance( $sidebar, $id ) { + list( $object, $number, $name ) = $this->get_widget_info( $id ); + if ( ! $object ) { + return array(); + } + + $object->_set( $number ); + + $instances = $object->get_settings(); + $instance = $instances[ $number ]; + + $args = array_merge( + is_array( $sidebar ) ? $sidebar : array(), + array( + 'widget_id' => $id, + 'widget_name' => $name, + ) + ); + + /** This filter is documented in wp-includes/class-wp-widget.php */ + $instance = apply_filters( 'widget_display_callback', $instance, $object, $args ); + + if ( false === $instance ) { + return array(); + } + + return $instance; + } + + /** + * Given a widget id returns an array containing information about the widget. + * + * @param string $widget_id Identifier of the widget. + * + * @return array Array containing the the widget object, the number, and the name. + */ + protected function get_widget_info( $widget_id ) { + global $wp_registered_widgets; + + if ( + ! is_array( $wp_registered_widgets[ $widget_id ]['callback'] ) || + ! isset( $wp_registered_widgets[ $widget_id ]['callback'][0] ) || + ! isset( $wp_registered_widgets[ $widget_id ]['params'][0]['number'] ) || + ! isset( $wp_registered_widgets[ $widget_id ]['name'] ) || + ! ( $wp_registered_widgets[ $widget_id ]['callback'][0] instanceof WP_Widget ) + ) { + return array( null, null, null ); + } + + $object = $wp_registered_widgets[ $widget_id ]['callback'][0]; + $number = $wp_registered_widgets[ $widget_id ]['params'][0]['number']; + $name = $wp_registered_widgets[ $widget_id ]['name']; + + return array( $object, $number, $name ); + } + + /** + * Checks if the given widget id is a reference widget, ie one that does not use WP_Widget. + * + * @since 5.6.0 + * + * @param string $widget_id The widget id to check. + * @return bool Whether this is a reference widget or not. + */ + protected function is_reference_widget( $widget_id ) { + list ( $object ) = $this->get_widget_info( $widget_id ); + + return ! $object instanceof WP_Widget; + } + + /** + * Gets the list of collection params. + * + * @since 5.6.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.', 'gutenberg' ), + 'type' => 'string', + ), + ); + } + + /** + * Retrieves the widget's schema, conforming to JSON Schema. + * + * @since 5.6.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.', 'gutenberg' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'id_base' => array( + 'description' => __( 'Type of widget for the object.', 'gutenberg' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'sidebar' => array( + 'description' => __( 'The sidebar the widget belongs to.', 'gutenberg' ), + 'type' => 'string', + 'default' => 'wp_inactive_widgets', + 'required' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'widget_class' => array( + 'description' => __( 'Class name of the widget implementation.', 'gutenberg' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'name' => array( + 'description' => __( 'Name of the widget.', 'gutenberg' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'description' => array( + 'description' => __( 'Description of the widget.', 'gutenberg' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'number' => array( + 'description' => __( 'Number of the widget.', 'gutenberg' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'rendered' => array( + 'description' => __( 'HTML representation of the widget.', 'gutenberg' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'rendered_form' => array( + 'description' => __( 'HTML representation of the widget admin form.', 'gutenberg' ), + 'type' => 'string', + 'context' => array( 'edit' ), + 'readonly' => true, + ), + 'settings' => array( + 'description' => __( 'Settings of the widget.', 'gutenberg' ), + 'type' => 'object', + 'context' => array( 'view', 'edit', 'embed' ), + 'default' => array(), + ), + ), + ); + + return $this->add_additional_fields_schema( $this->schema ); + } +} diff --git a/lib/load.php b/lib/load.php index 8397ec2562ef1e..98fa8aa28c2009 100644 --- a/lib/load.php +++ b/lib/load.php @@ -35,6 +35,9 @@ function gutenberg_is_experiment_enabled( $name ) { if ( ! class_exists( 'WP_REST_Sidebars_Controller' ) ) { require_once dirname( __FILE__ ) . '/class-wp-rest-sidebars-controller.php'; } + if ( ! class_exists( 'WP_REST_Widgets_Controller' ) ) { + require_once dirname( __FILE__ ) . '/class-wp-rest-widgets-controller.php'; + } if ( ! class_exists( 'WP_REST_Block_Directory_Controller' ) ) { require dirname( __FILE__ ) . '/class-wp-rest-block-directory-controller.php'; } diff --git a/lib/rest-api.php b/lib/rest-api.php index b4bca6d9af37d2..b9f7065d17be2b 100644 --- a/lib/rest-api.php +++ b/lib/rest-api.php @@ -189,13 +189,16 @@ function gutenberg_register_plugins_endpoint() { add_action( 'rest_api_init', 'gutenberg_register_plugins_endpoint' ); /** - * Registers the Sidebars REST API routes. + * Registers the Sidebars & Widgets REST API routes. */ -function gutenberg_register_sidebars_endpoint() { +function gutenberg_register_sidebars_and_widgets_endpoint() { $sidebars = new WP_REST_Sidebars_Controller(); $sidebars->register_routes(); + + $widgets = new WP_REST_Widgets_Controller(); + $widgets->register_routes(); } -add_action( 'rest_api_init', 'gutenberg_register_sidebars_endpoint' ); +add_action( 'rest_api_init', 'gutenberg_register_sidebars_and_widgets_endpoint' ); /** * Registers the Batch REST API routes. diff --git a/packages/edit-widgets/src/store/actions.js b/packages/edit-widgets/src/store/actions.js index 2884554b304552..e060b739a73c1c 100644 --- a/packages/edit-widgets/src/store/actions.js +++ b/packages/edit-widgets/src/store/actions.js @@ -102,7 +102,9 @@ export function* saveWidgetAreas( widgetAreas ) { KIND, WIDGET_AREA_ENTITY_TYPE, widgetArea.id, - { widgets: newWidgets } + { + widgets: newWidgets, + } ); yield* trySaveWidgetArea( widgetArea.id ); diff --git a/packages/edit-widgets/src/store/utils.js b/packages/edit-widgets/src/store/utils.js index 2ecdfc9cbc459a..1e53520832b703 100644 --- a/packages/edit-widgets/src/store/utils.js +++ b/packages/edit-widgets/src/store/utils.js @@ -41,7 +41,9 @@ export const buildWidgetAreasPostId = () => `widget-areas`; * @return {Object} Query. */ export function buildWidgetAreasQuery() { - return { per_page: -1 }; + return { + per_page: -1, + }; } /** diff --git a/phpunit/class-rest-sidebars-controller-test.php b/phpunit/class-rest-sidebars-controller-test.php index 4a14a2323784f1..cea5a984f4ba2a 100644 --- a/phpunit/class-rest-sidebars-controller-test.php +++ b/phpunit/class-rest-sidebars-controller-test.php @@ -145,8 +145,8 @@ private function setup_sidebar( $id, $attrs = array(), $widgets = array() ) { */ public function test_register_routes() { $routes = rest_get_server()->get_routes(); - $this->assertArrayHasKey( '/__experimental/sidebars', $routes ); - $this->assertArrayHasKey( '/__experimental/sidebars/(?P[\w-]+)', $routes ); + $this->assertArrayHasKey( '/wp/v2/sidebars', $routes ); + $this->assertArrayHasKey( '/wp/v2/sidebars/(?P[\w-]+)', $routes ); } /** @@ -159,7 +159,7 @@ public function test_context_param() { * */ public function test_get_items() { - $request = new WP_REST_Request( 'GET', '/__experimental/sidebars' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); @@ -171,7 +171,7 @@ public function test_get_items() { */ public function test_get_items_no_permission() { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/__experimental/sidebars' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'widgets_cannot_access', $response, 401 ); } @@ -181,7 +181,7 @@ public function test_get_items_no_permission() { */ public function test_get_items_wrong_permission_author() { wp_set_current_user( self::$author_id ); - $request = new WP_REST_Request( 'GET', '/__experimental/sidebars' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'widgets_cannot_access', $response, 403 ); } @@ -191,7 +191,7 @@ public function test_get_items_wrong_permission_author() { */ public function test_get_items_wrong_permission_subscriber() { wp_set_current_user( self::$subscriber_id ); - $request = new WP_REST_Request( 'GET', '/__experimental/sidebars' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'widgets_cannot_access', $response, 403 ); } @@ -207,17 +207,23 @@ public function test_get_items_basic_sidebar() { ) ); - $request = new WP_REST_Request( 'GET', '/__experimental/sidebars' ); + $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(), + 'id' => 'sidebar-1', + 'name' => 'Test sidebar', + 'description' => '', + 'status' => 'active', + 'widgets' => array(), + 'class' => '', + 'before_widget' => '', + 'after_widget' => '', + 'before_title' => '', + 'after_title' => '', ), ), $data @@ -250,96 +256,26 @@ public function test_get_items_active_sidebar_with_widgets() { array( 'text-1', 'rss-1' ) ); - $request = new WP_REST_Request( 'GET', '/__experimental/sidebars' ); + $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( - array( - 'id' => 'text-1', - 'settings' => array( - 'text' => 'Custom text test', - ), - 'id_base' => 'text', - 'widget_class' => 'WP_Widget_Text', - 'name' => 'Text', - 'description' => 'Arbitrary text.', - 'number' => 1, - 'rendered' => '
Custom text test
', - ), - array( - 'id' => 'rss-1', - 'settings' => array( - 'title' => 'RSS test', - ), - 'id_base' => 'rss', - 'widget_class' => 'WP_Widget_RSS', - 'name' => 'RSS', - 'description' => 'Entries from any RSS or Atom feed.', - 'number' => 1, - 'rendered' => '', - ), - ), - ), - ), - $data - ); - } - - /** - * Test a GET request in edit context. In particular, we expect rendered_form to be served correctly. - */ - public function test_get_items_active_sidebar_with_widgets_edit_context() { - $this->setup_widget( - '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', '/__experimental/sidebars' ); - $request['context'] = 'edit'; - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - $this->assertEquals( - array( - array( - 'id' => 'sidebar-1', - 'name' => 'Test sidebar', - 'description' => '', - 'status' => 'active', - 'widgets' => array( - array( - 'id' => 'text-1', - 'settings' => array( - 'text' => 'Custom text test', - ), - 'id_base' => 'text', - 'widget_class' => 'WP_Widget_Text', - 'name' => 'Text', - 'description' => 'Arbitrary text.', - 'number' => 1, - 'rendered' => '
Custom text test
', - 'rendered_form' => '' . "\n" . - ' ' . "\n" . - ' ' . "\n" . - ' ', - ), + '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 @@ -357,16 +293,22 @@ public function test_get_item() { ) ); - $request = new WP_REST_Request( 'GET', '/__experimental/sidebars/sidebar-1' ); + $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(), + 'id' => 'sidebar-1', + 'name' => 'Test sidebar', + 'description' => '', + 'status' => 'active', + 'widgets' => array(), + 'class' => '', + 'before_widget' => '', + 'after_widget' => '', + 'before_title' => '', + 'after_title' => '', ), $data ); @@ -384,7 +326,7 @@ public function test_get_item_no_permission() { ) ); - $request = new WP_REST_Request( 'GET', '/__experimental/sidebars/sidebar-1' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars/sidebar-1' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'widgets_cannot_access', $response, 401 ); } @@ -401,7 +343,7 @@ public function test_get_item_wrong_permission_author() { ) ); - $request = new WP_REST_Request( 'GET', '/__experimental/sidebars/sidebar-1' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars/sidebar-1' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'widgets_cannot_access', $response, 403 ); } @@ -418,7 +360,7 @@ public function test_get_item_wrong_permission_subscriber() { ) ); - $request = new WP_REST_Request( 'GET', '/__experimental/sidebars/sidebar-1' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars/sidebar-1' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'widgets_cannot_access', $response, 403 ); } @@ -447,125 +389,9 @@ public function test_update_item() { 'text' => 'Custom text test', ) ); - $this->setup_sidebar( - 'sidebar-1', - array( - 'name' => 'Test sidebar', - ), - array( 'text-1', 'rss-1' ) - ); - - $request = new WP_REST_Request( 'POST', '/__experimental/sidebars/sidebar-1' ); - $request->set_body_params( - array( - 'widgets' => array( - array( - 'id' => 'text-1', - 'settings' => array( - 'text' => 'Updated text test', - ), - 'id_base' => 'text', - 'widget_class' => 'WP_Widget_Text', - 'name' => 'Text', - 'description' => 'Arbitrary text.', - 'number' => 1, - ), - array( - 'id' => 'text-2', - 'settings' => array( - 'text' => 'Another text widget', - ), - 'id_base' => 'text', - 'widget_class' => 'WP_Widget_Text', - 'name' => 'Text', - 'description' => 'Arbitrary text.', - 'number' => 2, - ), - ), - ) - ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - $this->assertEquals( - array( - 'id' => 'sidebar-1', - 'name' => 'Test sidebar', - 'description' => '', - 'status' => 'active', - 'widgets' => array( - array( - 'id' => 'text-1', - 'settings' => array( - 'text' => 'Updated text test', - 'title' => '', - 'filter' => false, - ), - 'id_base' => 'text', - 'widget_class' => 'WP_Widget_Text', - 'name' => 'Text', - 'description' => 'Arbitrary text.', - 'number' => 1, - 'rendered' => '
Updated text test
', - ), - array( - 'id' => 'text-2', - 'settings' => array( - 'text' => 'Another text widget', - 'title' => '', - 'filter' => false, - ), - 'id_base' => 'text', - 'widget_class' => 'WP_Widget_Text', - 'name' => 'Text', - 'description' => 'Arbitrary text.', - 'number' => 2, - 'rendered' => '
Another text widget
', - ), - ), - ), - $data - ); - } - - /** - * @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( 'widget_text', - 1, + 2, array( 'text' => 'Custom text test', ) @@ -575,119 +401,36 @@ protected function update_text_widget_with_raw_html( $html ) { array( 'name' => 'Test sidebar', ), - array( 'text-1' ) - ); - - $request = new WP_REST_Request( 'POST', '/__experimental/sidebars/sidebar-1' ); - $request->set_body_params( - array( - 'widgets' => array( - array( - 'id' => 'text-1', - 'settings' => array( - 'text' => $html, - ), - 'id_base' => 'text', - 'widget_class' => 'WP_Widget_Text', - 'name' => 'Text', - 'description' => 'Arbitrary text.', - 'number' => 1, - ), - ), - ) - ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - return $data['widgets'][0]['rendered']; - } - - /** - * - */ - public function test_update_item_legacy_widget_1() { - $this->do_test_update_item_legacy_widget( 'testwidget-1' ); - } - - /** - * - */ - public function test_update_item_legacy_widget_2() { - $this->do_test_update_item_legacy_widget( 'testwidget' ); - } - - /** - * - */ - public function do_test_update_item_legacy_widget( $widget_id ) { - // @TODO: Use @dataProvider instead (it doesn't work with custom constructors like the one we have in this class) - wp_register_widget_control( - $widget_id, - '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 ); - } - }, - 100, - 200 - ); - wp_register_sidebar_widget( - $widget_id, - 'WP test widget', - function() { - $settings = get_option( 'widget_testwidget' ) ? get_option( 'widget_testwidget' ) : array( - 'id' => '', - 'title' => '', - ); - echo '

' . $settings['id'] . '

' . $settings['title'] . ''; - } - ); - $this->setup_sidebar( - 'sidebar-1', - array( - 'name' => 'Test sidebar', - ), - array( $widget_id ) + array( 'text-1', 'rss-1' ) ); - $request = new WP_REST_Request( 'POST', '/__experimental/sidebars/sidebar-1' ); + $request = new WP_REST_Request( 'PUT', '/wp/v2/sidebars/sidebar-1' ); $request->set_body_params( array( 'widgets' => array( - array( - 'id' => $widget_id, - 'name' => 'WP test widget', - 'settings' => array( - 'test_id' => 'My test id', - 'test_title' => 'My test title', - 'update_testwidget' => true, - ), - ), + '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( - array( - 'id' => $widget_id, - 'settings' => array(), - 'rendered' => '

My test id

My test title', - 'name' => 'WP test widget', - ), + '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 ); @@ -728,51 +471,40 @@ public function test_get_items_inactive_widgets() { ) ); - $request = new WP_REST_Request( 'GET', '/__experimental/sidebars' ); + $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( - array( - 'id' => 'text-1', - 'settings' => array( - 'text' => 'Custom text test', - ), - 'id_base' => 'text', - 'widget_class' => 'WP_Widget_Text', - 'name' => 'Text', - 'description' => 'Arbitrary text.', - 'number' => 1, - 'rendered' => '
Custom text test
', - ), + '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( - array( - 'id' => 'rss-1', - 'settings' => array( - 'title' => 'RSS test', - ), - 'id_base' => 'rss', - 'widget_class' => 'WP_Widget_RSS', - 'name' => 'RSS', - 'description' => 'Entries from any RSS or Atom feed.', - 'number' => 1, - 'rendered' => '', - ), + 'id' => 'wp_inactive_widgets', + 'name' => 'Inactive widgets', + 'description' => '', + 'status' => 'inactive', + 'widgets' => array( + 'rss-1', ), + 'class' => '', + 'before_widget' => '', + 'after_widget' => '', + 'before_title' => '', + 'after_title' => '', ), ), $data @@ -785,7 +517,7 @@ public function test_get_items_inactive_widgets() { public function test_update_item_no_permission() { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'POST', '/__experimental/sidebars/sidebar-1' ); + $request = new WP_REST_Request( 'POST', '/wp/v2/sidebars/sidebar-1' ); $request->set_body_params( array( 'widgets' => array(), @@ -801,7 +533,7 @@ public function test_update_item_no_permission() { public function test_update_item_wrong_permission_author() { wp_set_current_user( self::$author_id ); - $request = new WP_REST_Request( 'POST', '/__experimental/sidebars/sidebar-1' ); + $request = new WP_REST_Request( 'POST', '/wp/v2/sidebars/sidebar-1' ); $request->set_body_params( array( 'widgets' => array(), @@ -817,7 +549,7 @@ public function test_update_item_wrong_permission_author() { public function test_update_item_wrong_permission_subscriber() { wp_set_current_user( self::$subscriber_id ); - $request = new WP_REST_Request( 'POST', '/__experimental/sidebars/sidebar-1' ); + $request = new WP_REST_Request( 'POST', '/wp/v2/sidebars/sidebar-1' ); $request->set_body_params( array( 'widgets' => array(), @@ -827,60 +559,6 @@ public function test_update_item_wrong_permission_subscriber() { $this->assertErrorResponse( 'widgets_cannot_access', $response, 403 ); } - /** - * Tests if the endpoint correctly handles "slashable" characters such as " or '. - */ - public function test_update_item_slashing() { - $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( 'POST', '/__experimental/sidebars/sidebar-1' ); - $request->set_body_params( - array( - 'widgets' => array( - array( - 'id' => 'text-1', - 'settings' => array( - 'text' => 'Updated \\" \\\' text test', - ), - 'id_base' => 'text', - 'widget_class' => 'WP_Widget_Text', - 'name' => 'Text', - 'description' => 'Arbitrary text.', - 'number' => 1, - ), - ), - ) - ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - $this->assertEquals( - array( - 'id' => 'sidebar-1', - 'name' => 'Test sidebar', - 'description' => '', - 'status' => 'active', - 'widgets' => array( - array( - 'id' => 'text-1', - 'settings' => array( - 'text' => 'Updated \\" \\\' text test', - 'title' => '', - 'filter' => false, - ), - 'id_base' => 'text', - 'widget_class' => 'WP_Widget_Text', - 'name' => 'Text', - 'description' => 'Arbitrary text.', - 'number' => 1, - 'rendered' => '
Updated \\" \\\' text test
', - ), - ), - ), - $data - ); - } - /** * The test_delete_item() method does not exist for sidebar. */ @@ -898,16 +576,43 @@ public function test_prepare_item() { */ public function test_get_item_schema() { wp_set_current_user( self::$admin_id ); - $request = new WP_REST_Request( 'OPTIONS', '/__experimental/sidebars' ); + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/sidebars' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertEquals( 5, count( $properties ) ); + $this->assertEquals( 10, count( $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 ); + } + + /** + * 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/phpunit/class-rest-widgets-controller-test.php b/phpunit/class-rest-widgets-controller-test.php new file mode 100644 index 00000000000000..021b35b7620bc3 --- /dev/null +++ b/phpunit/class-rest-widgets-controller-test.php @@ -0,0 +1,1085 @@ +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() { + 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_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 + ); + + 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(); + } + } + } + + /** + * + */ + 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 ); + } + + /** + * + */ + public function test_context_param() { + } + + /** + * + */ + 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 ); + } + + /** + * + */ + 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 ); + } + + /** + * + */ + 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 ); + } + + /** + * + */ + public function test_get_items() { + $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/widgets' ); + $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', + 'settings' => array( + 'text' => 'Custom text test', + ), + 'id_base' => 'text', + 'widget_class' => 'WP_Widget_Text', + 'name' => 'Text', + 'description' => 'Arbitrary text.', + 'number' => 1, + 'rendered' => '
Custom text test
', + ), + array( + 'id' => 'rss-1', + 'sidebar' => 'sidebar-1', + 'settings' => array( + 'title' => 'RSS test', + ), + 'id_base' => 'rss', + 'widget_class' => 'WP_Widget_RSS', + 'name' => 'RSS', + 'description' => 'Entries from any RSS or Atom feed.', + 'number' => 1, + 'rendered' => '', + ), + ), + $data + ); + } + + /** + * Test a GET request in edit context. In particular, we expect rendered_form to be served correctly. + */ + public function test_get_items_edit_context() { + $this->setup_widget( + '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' ); + $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', + 'settings' => array( + 'text' => 'Custom text test', + ), + 'id_base' => 'text', + 'widget_class' => 'WP_Widget_Text', + 'name' => 'Text', + 'description' => 'Arbitrary text.', + 'number' => 1, + 'rendered' => '
Custom text test
', + 'rendered_form' => '' . "\n" . + ' ' . "\n" . + ' ' . "\n" . + ' ', + ), + ), + $data + ); + } + + /** + * + */ + public function test_get_item() { + $this->setup_widget( + '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', + 'settings' => array( + 'text' => 'Custom text test', + ), + 'id_base' => 'text', + 'widget_class' => 'WP_Widget_Text', + 'name' => 'Text', + 'description' => 'Arbitrary text.', + 'number' => 1, + 'rendered' => '
Custom text test
', + ), + $data + ); + } + + /** + * + */ + public function test_get_item_no_permission() { + wp_set_current_user( 0 ); + + $this->setup_widget( + '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 ); + } + + /** + * + */ + public function test_get_item_wrong_permission_author() { + wp_set_current_user( self::$author_id ); + $this->setup_widget( + '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 ); + } + + /** + * + */ + 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', + 'settings' => 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->assertEquals( 1, $data['number'] ); + $this->assertEqualSets( + array( + 'text' => 'Updated text test', + 'title' => '', + 'filter' => false, + ), + $data['settings'] + ); + } + + /** + * + */ + public function test_create_item_second_instance() { + $this->setup_widget( + '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', + 'settings' => 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->assertEquals( 2, $data['number'] ); + $this->assertEqualSets( + array( + 'text' => 'Updated text test', + 'title' => '', + 'filter' => false, + ), + $data['settings'] + ); + } + + /** + * + */ + public function test_update_item() { + $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( 'PUT', '/wp/v2/widgets/text-1' ); + $request->set_body_params( + array( + 'id' => 'text-1', + 'sidebar' => 'sidebar-1', + 'settings' => array( + 'text' => 'Updated text test', + ), + 'id_base' => 'text', + 'widget_class' => 'WP_Widget_Text', + 'name' => 'Text', + 'description' => 'Arbitrary text.', + 'number' => 1, + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'text-1', $data['id'] ); + $this->assertEquals( 'sidebar-1', $data['sidebar'] ); + $this->assertEquals( 1, $data['number'] ); + $this->assertEqualSets( + array( + 'text' => 'Updated text test', + 'title' => '', + 'filter' => false, + ), + $data['settings'] + ); + } + + /** + * + */ + public function test_update_item_reassign_sidebar() { + $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' ) + ); + $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'] ); + } + + /** + * @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( + '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', + 'settings' => array( + 'text' => $html, + ), + 'id_base' => 'text', + 'widget_class' => 'WP_Widget_Text', + 'name' => 'Text', + 'description' => 'Arbitrary text.', + 'number' => 1, + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + return $data['rendered']; + } + + /** + * + */ + public function test_update_item_legacy_widget_1() { + $this->do_test_update_item_legacy_widget( 'testwidget-1' ); + } + + /** + * + */ + public function test_update_item_legacy_widget_2() { + $this->do_test_update_item_legacy_widget( 'testwidget' ); + } + + /** + * + */ + public function do_test_update_item_legacy_widget( $widget_id ) { + // @TODO: Use @dataProvider instead (it doesn't work with custom constructors like the one we have in this class) + wp_register_widget_control( + $widget_id, + '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 ); + } + }, + 100, + 200 + ); + wp_register_sidebar_widget( + $widget_id, + 'WP test widget', + function () { + $settings = get_option( 'widget_testwidget' ) ? get_option( 'widget_testwidget' ) : array( + 'id' => '', + 'title' => '', + ); + echo '

' . $settings['id'] . '

' . $settings['title'] . ''; + } + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( $widget_id ) + ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/widgets/' . $widget_id ); + $request->set_body_params( + array( + 'id' => $widget_id, + 'name' => 'WP test widget', + 'settings' => array( + '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' => $widget_id, + 'sidebar' => 'sidebar-1', + 'settings' => array(), + 'rendered' => '

My test id

My test title', + 'name' => 'WP test widget', + 'number' => 0, + 'rendered_form' => '', + 'widget_class' => '', + 'id_base' => '', + 'description' => '', + ), + $data + ); + } + + /** + * + */ + public function test_create_item_legacy_widget_1() { + $this->do_test_create_item_legacy_widget( 'testwidget-1' ); + } + + /** + * + */ + public function test_create_item_legacy_widget_2() { + $this->do_test_create_item_legacy_widget( 'testwidget' ); + } + + /** + * + */ + public function do_test_create_item_legacy_widget( $widget_id ) { + // @TODO: Use @dataProvider instead (it doesn't work with custom constructors like the one we have in this class) + wp_register_widget_control( + $widget_id, + '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 ); + } + }, + 100, + 200 + ); + wp_register_sidebar_widget( + $widget_id, + 'WP test widget', + function () { + $settings = get_option( 'widget_testwidget' ) ? get_option( 'widget_testwidget' ) : array( + 'id' => '', + 'title' => '', + ); + echo '

' . $settings['id'] . '

' . $settings['title'] . ''; + } + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array() + ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/widgets/' . $widget_id ); + $request->set_body_params( + array( + 'id' => $widget_id, + 'sidebar' => 'sidebar-1', + 'name' => 'WP test widget', + 'settings' => array( + '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' => $widget_id, + 'sidebar' => 'sidebar-1', + 'settings' => array(), + 'rendered' => '

My test id

My test title', + 'name' => 'WP test widget', + 'number' => 0, + 'rendered_form' => '', + 'widget_class' => '', + 'id_base' => '', + 'description' => '', + ), + $data + ); + } + + /** + * + */ + 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( 'widgets_cannot_access', $response, 401 ); + } + + /** + * + */ + 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( 'widgets_cannot_access', $response, 403 ); + } + + /** + * Tests if the endpoint correctly handles "slashable" characters such as " or '. + */ + public function test_update_item_slashing() { + $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( 'PUT', '/wp/v2/widgets/text-1' ); + $request->set_body_params( + array( + 'id' => 'text-1', + 'sidebar' => 'sidebar-1', + 'settings' => array( + 'text' => 'Updated \\" \\\' text test', + ), + 'id_base' => 'text', + 'widget_class' => 'WP_Widget_Text', + 'name' => 'Text', + 'description' => 'Arbitrary text.', + 'number' => 1, + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEqualSets( + array( + 'text' => 'Updated \\" \\\' text test', + 'title' => '', + 'filter' => false, + ), + $data['settings'] + ); + + $this->assertEquals( + '
Updated \\" \\\' text test
', + $data['rendered'] + ); + } + + /** + * + */ + public function test_delete_item() { + $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( 'DELETE', '/wp/v2/widgets/text-1' ); + $response = rest_do_request( $request ); + + $this->assertEqualSets( + array( + 'id' => 'text-1', + 'sidebar' => 'wp_inactive_widgets', + 'settings' => array( + 'text' => 'Custom text test', + ), + 'id_base' => 'text', + 'widget_class' => 'WP_Widget_Text', + 'name' => 'Text', + 'description' => 'Arbitrary text.', + 'number' => 1, + 'rendered' => '', + 'rendered_form' => '' . "\n" . + ' ' . "\n" . + ' ' . "\n" . + ' ', + ), + $response->get_data() + ); + } + + /** + * + */ + public function test_delete_item_force() { + $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( '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', + 'settings' => array( + 'text' => 'Custom text test', + ), + 'id_base' => 'text', + 'widget_class' => 'WP_Widget_Text', + 'name' => 'Text', + 'description' => 'Arbitrary text.', + 'number' => 1, + '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() ); + } + + /** + * + */ + public function test_delete_item_logged_out() { + wp_set_current_user( 0 ); + + $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( 'DELETE', '/wp/v2/widgets/text-1' ); + $response = rest_do_request( $request ); + + $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); + } + + /** + * + */ + public function test_delete_item_author() { + wp_set_current_user( self::$author_id ); + + $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( '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() { + } + + /** + * + */ + 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( 10, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'id_base', $properties ); + $this->assertArrayHasKey( 'sidebar', $properties ); + $this->assertArrayHasKey( 'widget_class', $properties ); + $this->assertArrayHasKey( 'name', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'number', $properties ); + $this->assertArrayHasKey( 'rendered', $properties ); + $this->assertArrayHasKey( 'rendered_form', $properties ); + $this->assertArrayHasKey( 'settings', $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; + } +}