diff --git a/gutenberg.php b/gutenberg.php index 55579611942c..c9269207a4ab 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -181,3 +181,14 @@ function register_site_icon_url( $response ) { } add_filter( 'rest_index', 'register_site_icon_url' ); + +/** + * Registers the WP_Widget_Block widget + */ +function gutenberg_register_widgets() { + if ( gutenberg_is_experiment_enabled( 'gutenberg-widget-experiments' ) ) { + register_widget( 'WP_Widget_Block' ); + } +} + +add_action( 'widgets_init', 'gutenberg_register_widgets' ); diff --git a/lib/class-experimental-wp-widget-blocks-manager.php b/lib/class-experimental-wp-widget-blocks-manager.php deleted file mode 100644 index c915807bd769..000000000000 --- a/lib/class-experimental-wp-widget-blocks-manager.php +++ /dev/null @@ -1,420 +0,0 @@ - $post_id, - 'wp_inactive_widgets' => array_merge( - $sidebars['wp_inactive_widgets'], - $sidebar - ), - ) - ) - ); - } - - /** - * Returns a sidebar as an array of legacy widget blocks. - * - * @since 5.7.0 - * - * @param string $sidebar_id Identifier of the sidebar. - * @return array $post_id Post id. - */ - public static function get_sidebar_as_blocks( $sidebar_id ) { - $blocks = array(); - - $sidebars_items = self::get_raw_sidebar_widgets(); - $wp_registered_sidebars = self::get_wp_registered_sidebars(); - - foreach ( $sidebars_items[ $sidebar_id ] as $item ) { - $widget_class = self::get_widget_class( $item ); - list( $object, $number ) = self::get_widget_info( $item ); - $new_block = array( - 'blockName' => 'core/legacy-widget', - 'attrs' => array( - 'id' => $item, - 'instance' => self::get_sidebar_widget_instance( $wp_registered_sidebars[ $sidebar_id ], $item ), - ), - 'innerHTML' => '', - ); - if ( null !== $widget_class ) { - $new_block['attrs']['widgetClass'] = $widget_class; - } - if ( isset( $object->id_base ) ) { - $new_block['attrs']['idBase'] = $object->id_base; - } - if ( is_int( $number ) ) { - $new_block['attrs']['number'] = $number; - } - $blocks[] = $new_block; - } - return $blocks; - } - - /** - * Verifies if a sidebar id is valid or not. - * - * @since 5.7.0 - * - * @param string $sidebar_id Identifier of the sidebar. - * @return boolean True if the $sidebar_id value is valid and false otherwise. - */ - public static function is_valid_sidebar_id( $sidebar_id ) { - $wp_registered_sidebars = self::get_wp_registered_sidebars(); - return isset( $wp_registered_sidebars[ $sidebar_id ] ); - } - - - /** - * Given a widget id returns the name of the class the represents the widget. - * - * @since 5.7.0 - * - * @param string $widget_id Identifier of the widget. - * @return string|null Name of the class that represents the widget or null if the widget is not represented by a class. - */ - private static function get_widget_class( $widget_id ) { - $wp_registered_widgets = self::get_wp_registered_widgets(); - if ( - isset( $wp_registered_widgets[ $widget_id ]['callback'][0] ) && - $wp_registered_widgets[ $widget_id ]['callback'][0] instanceof WP_Widget - ) { - return get_class( $wp_registered_widgets[ $widget_id ]['callback'][0] ); - } - return null; - } - - /** - * Retrieves a widget instance. - * - * @since 5.7.0 - * - * @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. - */ - private static function get_sidebar_widget_instance( $sidebar, $id ) { - list( $object, $number, $name ) = self::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. - * - * @since 2.8.0 - * - * @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. - */ - $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. - * - * @since 5.7.0 - * - * @param string $widget_id Identifier of the widget. - * @return array Array containing the the widget object, the number, and the name. - */ - private static function get_widget_info( $widget_id ) { - $wp_registered_widgets = self::get_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 ); - } - - /** - * Serializes an array of blocks. - * - * @since 5.7.0 - * - * @param array $blocks Post Array of block objects. - * @return string String representing the blocks. - */ - public static function serialize_blocks( $blocks ) { - return implode( array_map( 'self::serialize_block', $blocks ) ); - } - - /** - * Serializes a block. - * - * @since 5.7.0 - * - * @param array $block Block object. - * @return string String representing the block. - */ - public static function serialize_block( $block ) { - if ( ! isset( $block['blockName'] ) ) { - return false; - } - $name = $block['blockName']; - if ( 0 === strpos( $name, 'core/' ) ) { - $name = substr( $name, strlen( 'core/' ) ); - } - - if ( empty( $block['attrs'] ) ) { - $opening_tag_suffix = ''; - } else { - $opening_tag_suffix = ' ' . json_encode( $block['attrs'] ); - } - - if ( empty( $block['innerHTML'] ) ) { - return sprintf( - '', - $name, - $opening_tag_suffix - ); - } else { - return sprintf( - '%3$s', - $name, - $opening_tag_suffix, - $block['innerHTML'] - ); - } - } - - /** - * Outputs a block widget on the website frontend. - * - * @param array $options Widget options. - * @param array $arguments Arguments array. - */ - public static function output_blocks_widget( $options, $arguments ) { - echo $options['before_widget']; - foreach ( $arguments['blocks'] as $block ) { - echo render_block( $block ); - } - echo $options['after_widget']; - } - - /** - * Noop block widget control output function for the necessary call to `wp_register_widget_control`. - */ - public static function output_blocks_widget_control() {} - - /** - * Registers a widget that should represent a set of blocks and returns its ID. - * - * @param array $blocks Array of blocks. - */ - public static function convert_blocks_to_widget( $blocks ) { - $widget_id = 'blocks-widget-' . md5( self::serialize_blocks( $blocks ) ); - global $wp_registered_widgets; - if ( isset( $wp_registered_widgets[ $widget_id ] ) ) { - return $widget_id; - } - wp_register_sidebar_widget( - $widget_id, - __( 'Blocks Area', 'gutenberg' ), - 'Experimental_WP_Widget_Blocks_Manager::output_blocks_widget', - array( - 'classname' => 'widget-area', - 'description' => __( 'Displays a set of blocks', 'gutenberg' ), - ), - array( - 'blocks' => $blocks, - ) - ); - wp_register_widget_control( - $widget_id, - __( 'Blocks Area', 'gutenberg' ), - 'Experimental_WP_Widget_Blocks_Manager::output_blocks_widget_control', - array( 'id_base' => 'blocks-widget' ) - ); - return $widget_id; - } - - /** - * Filters the $sidebars_widgets to exchange wp_area post id with a widget that renders that block area. - * - * @param array $sidebars_widgets_input An associative array of sidebars and their widgets. - */ - public static function swap_out_sidebars_blocks_for_block_widgets( $sidebars_widgets_input ) { - global $sidebars_widgets; - global $wp_customize; - if ( null === self::$unfiltered_sidebar_widgets ) { - self::$unfiltered_sidebar_widgets = $sidebars_widgets; - } - $changeset_data = null; - if ( function_exists( 'is_customize_preview' ) && is_customize_preview() ) { - $changeset_data = $wp_customize->changeset_data(); - if ( isset( $changeset_data['gutenberg_widget_blocks']['value'] ) ) { - $changeset_data = json_decode( $changeset_data['gutenberg_widget_blocks']['value'] ); - } - } - - $filtered_sidebar_widgets = array(); - foreach ( $sidebars_widgets_input as $sidebar_id => $item ) { - $changeset_value = $changeset_data && isset( $changeset_data->$sidebar_id ) - ? $changeset_data->$sidebar_id - : null; - - if ( ! is_numeric( $item ) && ! $changeset_value ) { - $filtered_sidebar_widgets[ $sidebar_id ] = $item; - continue; - } - - $filtered_widgets = array(); - $last_set_of_blocks = array(); - $blocks = parse_blocks( - $changeset_value ? $changeset_value : get_post( $item )->post_content - ); - - foreach ( $blocks as $block ) { - if ( ! isset( $block['blockName'] ) ) { - continue; - } - if ( - 'core/legacy-widget' === $block['blockName'] && - isset( $block['attrs']['identifier'] ) - ) { - if ( ! empty( $last_set_of_blocks ) ) { - $filtered_widgets[] = self::convert_blocks_to_widget( $last_set_of_blocks ); - $last_set_of_blocks = array(); - } - $filtered_widgets[] = $block['attrs']['identifier']; - } else { - $last_set_of_blocks[] = $block; - } - } - if ( ! empty( $last_set_of_blocks ) ) { - $filtered_widgets[] = self::convert_blocks_to_widget( $last_set_of_blocks ); - } - - $filtered_sidebar_widgets[ $sidebar_id ] = $filtered_widgets; - } - $sidebars_widgets = $filtered_sidebar_widgets; - - return $filtered_sidebar_widgets; - } -} diff --git a/lib/class-wp-rest-sidebars-controller.php b/lib/class-wp-rest-sidebars-controller.php new file mode 100644 index 000000000000..900df70340c1 --- /dev/null +++ b/lib/class-wp-rest-sidebars-controller.php @@ -0,0 +1,597 @@ +. + * + * @author Martin Pettersson + * @copyright 2015 Martin Pettersson + * @license GPLv2 + * @link https://github.com/martin-pettersson/wp-rest-api-sidebars + * @package gutenberg + */ + +/** + * Class Sidebars_Controller + * + * @package WP_API_Sidebars\Controllers + */ +class WP_REST_Sidebars_Controller extends WP_REST_Controller { + + /** + * Plugins controller constructor. + * + * @since 5.5.0 + */ + public function __construct() { + $this->namespace = '__experimental'; + $this->rest_base = 'sidebars'; + } + + /** + * Registers the controllers routes. + * + * @return void + */ + public function register_routes() { + // Lists all sidebars. + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + // Lists/updates a single sidebar based on the given id. + 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, '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]; + }, + ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * 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 + * @access public + */ + public function permissions_check() { + // Verify if the current user has edit_theme_options capability. + // This capability is required to access the widgets screen. + if ( ! current_user_can( 'edit_theme_options' ) ) { + return new WP_Error( + 'widgets_cannot_access', + __( 'Sorry, you are not allowed to access widgets on this site.', 'gutenberg' ), + array( + 'status' => rest_authorization_required_code(), + ) + ); + } + + return true; + } + + + /** + * Updates the sidebar. + * + * @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( $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; + } + } + } elseif ( $wp_registered_widget_updates[ $input_widget['id'] ] ) { + // Old-style widget. + $update_control = $wp_registered_widget_updates[ $input_widget['id'] ]; + $_POST = wp_slash( $input_widget['settings'] ); + call_user_func( $update_control['callback'] ); + } + ob_end_clean(); + + $sidebar_widgets_ids[] = $input_widget['id']; + } + + // 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 ); + } + + /** + * Returns a list of sidebars (active or inactive) + * + * @param WP_REST_Request $request The request instance. + * + * @return WP_REST_Response + * @global array $wp_registered_sidebars + */ + public function get_items( $request ) { + $data = array(); + foreach ( (array) wp_get_sidebars_widgets() as $id => $widgets ) { + $sidebar = self::get_sidebar( $id )[1]; + + $data[] = $this->prepare_item_for_response( $sidebar, $request )->get_data(); + } + return rest_ensure_response( $data ); + } + + /** + * Returns the given 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]; + + return $this->prepare_item_for_response( $sidebar, $request ); + } + + /** + * Returns a sidebar for the given id or null if not found + * + * Note: The id can be either an index, the id or the name of a sidebar + * + * @param string|int $id ID of the sidebar. + * + * @return array|null + * @global array $wp_registered_sidebars + */ + public static function get_sidebar( $id ) { + global $wp_registered_sidebars; + + if ( is_int( $id ) ) { + $id = 'sidebar-' . $id; + } else { + $id = sanitize_title( $id ); + + foreach ( (array) $wp_registered_sidebars as $key => $sidebar ) { + if ( sanitize_title( $sidebar['name'] ) === $id ) { + return array( true, $sidebar ); + } + } + } + + foreach ( (array) $wp_registered_sidebars as $key => $sidebar ) { + if ( $key === $id ) { + return array( true, $sidebar ); + } + } + + return array( false, array( 'id' => $id ) ); + } + + /** + * Returns a list of widgets for the given sidebar id + * + * @param string $sidebar_id ID of the sidebar. + * + * @return array + * @global array $wp_registered_widgets + * @global array $wp_registered_sidebars + */ + public static function get_widgets( $sidebar_id ) { + global $wp_registered_widgets, $wp_registered_sidebars; + + $widgets = array(); + $sidebars_widgets = (array) wp_get_sidebars_widgets(); + + if ( isset( $wp_registered_sidebars[ $sidebar_id ] ) && 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( + $wp_registered_sidebars[ $sidebar_id ], + 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'] = 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( + $wp_registered_sidebars[ $sidebar_id ], + $widget_id + ); + $widget['number'] = (int) $widget['params'][0]['number']; + $widget['id_base'] = $instance->id_base; + } + + 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. + * + * @return WP_REST_Response $data + */ + public function prepare_item_for_response( $raw_sidebar, $request ) { + global $wp_registered_sidebars; + + $id = $raw_sidebar['id']; + $sidebar = array( 'id' => $id ); + + if ( isset( $wp_registered_sidebars[ $id ] ) ) { + $registered_sidebar = $wp_registered_sidebars[ $id ]; + + $sidebar['status'] = 'active'; + $sidebar['name'] = isset( $registered_sidebar['name'] ) ? $registered_sidebar['name'] : ''; + $sidebar['description'] = isset( $registered_sidebar['description'] ) ? $registered_sidebar['description'] : ''; + } else { + $sidebar['status'] = 'inactive'; + } + + $fields = $this->get_fields_for_response( $request ); + if ( rest_is_field_included( 'widgets', $fields ) ) { + $sidebar['widgets'] = self::get_widgets( $sidebar['id'] ); + } + + $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'] ) { + $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 && '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; + } + + $response = rest_ensure_response( $data ); + + /** + * Filters a sidebar location returned from the REST API. + * + * Allows modification of the menu location data right before it is + * 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. + */ + return apply_filters( 'rest_prepare_sidebar', $response, $sidebar, $request ); + } + + /** + * Retrieves the block type' schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'sidebar', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'ID of sidebar.', 'gutenberg' ), + 'type' => 'string', + 'default' => '', + 'context' => array( 'embed', 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Unique name identifying the sidebar.', 'gutenberg' ), + 'type' => 'string', + 'default' => '', + 'context' => array( 'embed', 'view', 'edit' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'Description of sidebar.', 'gutenberg' ), + 'type' => 'string', + 'default' => '', + 'context' => array( 'embed', 'view', 'edit' ), + 'readonly' => true, + ), + 'status' => array( + 'description' => __( 'Status of sidebar.', 'gutenberg' ), + 'type' => 'string', + 'enum' => array( 'active', 'inactive' ), + 'default' => '', + 'context' => array( 'embed', 'view', 'edit' ), + 'readonly' => true, + ), + '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', 'edit', 'embed' ), + 'readonly' => true, + ), + 'settings' => array( + 'description' => __( 'Settings of the widget.', 'gutenberg' ), + 'type' => 'object', + 'context' => array( 'view', 'edit', 'embed' ), + 'default' => array(), + ), + ), + ), + 'default' => array(), + 'context' => array( 'embed', 'view', 'edit' ), + ), + ), + ); + + $this->schema = $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-widget-areas-controller.php b/lib/class-wp-rest-widget-areas-controller.php deleted file mode 100644 index d03bf04435ea..000000000000 --- a/lib/class-wp-rest-widget-areas-controller.php +++ /dev/null @@ -1,275 +0,0 @@ -namespace = '__experimental'; - $this->rest_base = 'widget-areas'; - } - - /** - * Registers the necessary REST API routes. - * - * @access public - */ - 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' ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - - $id_argument = array( - 'description' => __( 'The sidebar’s ID.', 'gutenberg' ), - 'type' => 'string', - 'required' => true, - 'validate_callback' => 'Experimental_WP_Widget_Blocks_Manager::is_valid_sidebar_id', - ); - - register_rest_route( - $this->namespace, - '/' . $this->rest_base . '/(?P.+)', - array( - 'args' => array( - 'id' => $id_argument, - ), - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_items_permissions_check' ), - ), - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'update_item' ), - 'permission_callback' => array( $this, 'update_item_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - } - - /** - * Retrieves the comment's schema, conforming to JSON Schema. - * - * @since 6.1.0 - * - * @return array - */ - public function get_item_schema() { - $schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'widget-area', - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Unique identifier for the object.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'view', 'edit', 'embed' ), - 'readonly' => true, - ), - 'content' => array( - 'description' => __( 'The content for the object.', 'gutenberg' ), - 'type' => 'object', - 'context' => array( 'view', 'edit', 'embed' ), - 'arg_options' => array( - 'sanitize_callback' => null, - 'validate_callback' => null, - ), - 'properties' => array( - 'raw' => array( - 'description' => __( 'Content for the object, as it exists in the database.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'rendered' => array( - 'description' => __( 'HTML content for the object, transformed for display.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'view', 'edit', 'embed' ), - 'readonly' => true, - ), - 'block_version' => array( - 'description' => __( 'Version of the content block format used by the object.', 'gutenberg' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit', 'embed' ), - 'readonly' => true, - ), - ), - ), - ), - ); - - return $schema; - } - - /** - * Checks whether a given request has permission to read widget areas. - * - * @since 5.7.0 - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|bool True if the request has read access, WP_Error object otherwise. - * - * This function is overloading a function defined in WP_REST_Controller so it should have the same parameters. - * phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - */ - public function get_items_permissions_check( $request ) { - if ( ! current_user_can( 'edit_theme_options' ) ) { - return new WP_Error( - 'rest_user_cannot_view', - __( 'Sorry, you are not allowed to read sidebars.', 'gutenberg' ) - ); - } - - return true; - } - /* phpcs:enable */ - - /** - * Retrieves all widget areas. - * - * @since 5.7.0 - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure. - */ - public function get_items( $request ) { - global $wp_registered_sidebars; - - $data = array(); - - foreach ( array_keys( $wp_registered_sidebars ) as $sidebar_id ) { - $data[ $sidebar_id ] = $this->get_sidebar_data( $sidebar_id ); - } - - return rest_ensure_response( $data ); - } - - /** - * Retrieves a specific widget area. - * - * @since 5.7.0 - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure. - */ - public function get_item( $request ) { - return rest_ensure_response( $this->get_sidebar_data( $request['id'] ) ); - } - - /** - * Checks if a given REST request has access to update a widget area. - * - * @since 5.7.0 - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|bool True if the request has access to update the item, error object otherwise. - * - * This function is overloading a function defined in WP_REST_Controller so it should have the same parameters. - * phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - */ - public function update_item_permissions_check( $request ) { - if ( ! current_user_can( 'edit_theme_options' ) ) { - return new WP_Error( - 'rest_user_cannot_edit', - __( 'Sorry, you are not allowed to edit sidebars.', 'gutenberg' ) - ); - } - - return true; - } - /* phpcs:enable */ - - /** - * Updates a single widget area. - * - * @since 5.7.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 ) { - $sidebar_id = $request->get_param( 'id' ); - $sidebar_content = $request->get_param( 'content' ); - - if ( ! is_string( $sidebar_content ) && isset( $sidebar_content['raw'] ) ) { - $sidebar_content = $sidebar_content['raw']; - } - - $id_referenced_in_sidebar = Experimental_WP_Widget_Blocks_Manager::get_post_id_referenced_in_sidebar( $sidebar_id ); - - $post_id = wp_insert_post( - array( - 'ID' => $id_referenced_in_sidebar, - 'post_content' => $sidebar_content, - 'post_type' => 'wp_area', - ) - ); - - if ( 0 === $id_referenced_in_sidebar ) { - Experimental_WP_Widget_Blocks_Manager::reference_post_id_in_sidebar( $sidebar_id, $post_id ); - } - - return rest_ensure_response( $this->get_sidebar_data( $request['id'] ) ); - } - - /** - * Returns the sidebar data together with a content array containing the blocks present in the sidebar. - * The bocks may be legacy widget blocks representing the widgets currently present in the sidebar, or the content of a wp_area post that the sidebar references. - * - * @since 5.7.0 - * - * @param string $sidebar_id Identifier of the sidebar. - * @return object Sidebar data with a content array. - */ - protected function get_sidebar_data( $sidebar_id ) { - $content_string = ''; - - $post_id_referenced_in_sidebar = Experimental_WP_Widget_Blocks_Manager::get_post_id_referenced_in_sidebar( $sidebar_id ); - - if ( 0 !== $post_id_referenced_in_sidebar ) { - $post = get_post( $post_id_referenced_in_sidebar ); - $content_string = $post->post_content; - } else { - $blocks = Experimental_WP_Widget_Blocks_Manager::get_sidebar_as_blocks( $sidebar_id ); - $content_string = Experimental_WP_Widget_Blocks_Manager::serialize_blocks( $blocks ); - } - - return array_merge( - Experimental_WP_Widget_Blocks_Manager::get_wp_registered_sidebars_sidebar( $sidebar_id ), - array( - 'content' => array( - 'raw' => $content_string, - 'rendered' => apply_filters( 'the_content', $content_string ), - 'block_version' => block_version( $content_string ), - ), - ) - ); - } -} diff --git a/lib/class-wp-rest-widget-forms.php b/lib/class-wp-rest-widget-utils-controller.php similarity index 54% rename from lib/class-wp-rest-widget-forms.php rename to lib/class-wp-rest-widget-utils-controller.php index 6ff3d16c0b16..832a25a62528 100644 --- a/lib/class-wp-rest-widget-forms.php +++ b/lib/class-wp-rest-widget-utils-controller.php @@ -14,7 +14,7 @@ * * @see WP_REST_Controller */ -class WP_REST_Widget_Forms extends WP_REST_Controller { +class WP_REST_Widget_Utils_Controller extends WP_REST_Controller { /** * Constructs the controller. @@ -23,7 +23,7 @@ class WP_REST_Widget_Forms extends WP_REST_Controller { */ public function __construct() { $this->namespace = '__experimental'; - $this->rest_base = 'widget-forms'; + $this->rest_base = 'widget-utils'; } /** @@ -32,46 +32,40 @@ public function __construct() { * @access public */ public function register_routes() { - register_rest_route( - $this->namespace, - '/' . $this->rest_base . '/(?P[^/]*)/', - array( - 'args' => array( - 'widget_class' => array( - 'description' => __( 'Class name of the widget.', 'gutenberg' ), - 'type' => 'string', - 'required' => true, - 'validate_callback' => array( $this, 'is_valid_widget' ), - ), - 'instance' => array( - 'description' => __( 'Current widget instance', 'gutenberg' ), - 'type' => 'object', - 'default' => array(), - ), - 'instance_changes' => array( - 'description' => __( 'Array of instance changes', 'gutenberg' ), - 'type' => 'object', - 'default' => array(), - ), + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/form/(?P[^/]*)/', + array( + 'args' => array( + 'widget_class' => array( + 'description' => __( 'Class name of the widget.', 'gutenberg' ), + 'type' => 'string', + 'required' => true, + 'validate_callback' => array( $this, 'is_valid_widget' ), ), - array( - 'methods' => WP_REST_Server::EDITABLE, - 'permission_callback' => array( $this, 'compute_new_widget_permissions_check' ), - 'callback' => array( $this, 'compute_new_widget' ), + 'instance' => array( + 'description' => __( 'Current widget instance', 'gutenberg' ), + 'type' => 'object', + 'default' => array(), ), - ) - ); + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'permission_callback' => array( $this, 'permissions_check' ), + 'callback' => array( $this, 'compute_widget_form' ), + ), + ) + ); } /** * 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.2.0 * @access public - * - * @return true|WP_Error True if the request has read access, WP_Error object otherwise. */ - public function compute_new_widget_permissions_check() { + public function permissions_check() { // Verify if the current user has edit_theme_options capability. // This capability is required to access the widgets screen. if ( ! current_user_can( 'edit_theme_options' ) ) { @@ -83,16 +77,17 @@ public function compute_new_widget_permissions_check() { ) ); } + return true; } /** * Checks if the widget being referenced is valid. * - * @since 5.2.0 * @param string $widget_class Name of the class the widget references. * * @return boolean| True if the widget being referenced exists and false otherwise. + * @since 5.2.0 */ private function is_valid_widget( $widget_class ) { $widget_class = urldecode( $widget_class ); @@ -100,6 +95,7 @@ private function is_valid_widget( $widget_class ) { if ( ! $widget_class ) { return false; } + return isset( $wp_widget_factory->widgets[ $widget_class ] ) && ( $wp_widget_factory->widgets[ $widget_class ] instanceof WP_Widget ); } @@ -107,16 +103,15 @@ private function is_valid_widget( $widget_class ) { /** * Returns the new widget instance and the form that represents it. * - * @since 5.7.0 - * @access public - * * @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. + * @since 5.7.0 + * @access public */ - public function compute_new_widget( $request ) { - $widget_class = urldecode( $request->get_param( 'widget_class' ) ); - $instance = $request->get_param( 'instance' ); - $instance_changes = $request->get_param( 'instance_changes' ); + public function compute_widget_form( $request ) { + $widget_class = urldecode( $request->get_param( 'widget_class' ) ); + $instance = $request->get_param( 'instance' ); global $wp_widget_factory; $widget_obj = $wp_widget_factory->widgets[ $widget_class ]; @@ -124,29 +119,6 @@ public function compute_new_widget( $request ) { $widget_obj->_set( -1 ); ob_start(); - if ( ! empty( $instance_changes ) ) { - $old_instance = $instance; - $instance = $widget_obj->update( $instance_changes, $old_instance ); - - /** - * Filters a widget's settings before saving. - * - * Returning false will effectively short-circuit the widget's ability - * to update settings. The old setting will be returned. - * - * @since 5.2.0 - * - * @param array $instance The current widget instance's settings. - * @param array $instance_changes Array of new widget settings. - * @param array $old_instance Array of old widget settings. - * @param WP_Widget $widget_ob The widget instance. - */ - $instance = apply_filters( 'widget_update_callback', $instance, $instance_changes, $old_instance, $widget_obj ); - if ( false === $instance ) { - $instance = $old_instance; - } - } - $instance = apply_filters( 'widget_form_callback', $instance, $widget_obj ); $return = null; @@ -163,11 +135,11 @@ public function compute_new_widget( $request ) { * Note: If the widget has no form, the text echoed from the default * form method can be hidden using CSS. * - * @since 5.2.0 + * @param WP_Widget $widget_obj The widget instance (passed by reference). + * @param null $return Return null if new fields are added. + * @param array $instance An array of the widget's settings. * - * @param WP_Widget $widget_obj The widget instance (passed by reference). - * @param null $return Return null if new fields are added. - * @param array $instance An array of the widget's settings. + * @since 5.2.0 */ do_action_ref_array( 'in_widget_form', array( &$widget_obj, &$return, $instance ) ); } @@ -180,6 +152,7 @@ public function compute_new_widget( $request ) { ) ); } + } /** * End: Include for phase 2 diff --git a/lib/class-wp-widget-block.php b/lib/class-wp-widget-block.php new file mode 100644 index 000000000000..3d0d23422158 --- /dev/null +++ b/lib/class-wp-widget-block.php @@ -0,0 +1,89 @@ + '', + ); + + /** + * Sets up a new Block widget instance. + * + * @since 4.8.1 + */ + public function __construct() { + $widget_ops = array( + 'classname' => 'widget_block', + 'description' => __( 'Gutenberg block.', 'gutenberg' ), + 'customize_selective_refresh' => true, + ); + $control_ops = array( + 'width' => 400, + 'height' => 350, + ); + parent::__construct( 'block', __( 'Gutenberg Block', 'gutenberg' ), $widget_ops, $control_ops ); + } + + /** + * Outputs the content for the current Block widget instance. + * + * @since 4.8.1 + * + * @global WP_Post $post Global post object. + * + * @param array $args Display arguments including 'before_title', 'after_title', + * 'before_widget', and 'after_widget'. + * @param array $instance Settings for the current Block widget instance. + */ + public function widget( $args, $instance ) { + echo do_blocks( $instance['content'] ); + } + + /** + * Handles updating settings for the current Block widget instance. + * + * @since 4.8.1 + * + * @param array $new_instance New settings for this instance as input by the user via + * WP_Widget::form(). + * @param array $old_instance Old settings for this instance. + * @return array Settings to save or bool false to cancel saving. + */ + public function update( $new_instance, $old_instance ) { + $instance = array_merge( $this->default_instance, $old_instance ); + $instance['content'] = $new_instance['content']; + return $instance; + } + + /** + * Outputs the Block widget settings form. + * + * @see WP_Widget_Custom_HTML::render_control_template_scripts() + * + * @param array $instance Current instance. + */ + public function form( $instance ) { + $instance = wp_parse_args( (array) $instance, $this->default_instance ); + echo do_blocks( $instance['content'] ); + ?> + + $sidebar_content ) { - $id_referenced_in_sidebar = Experimental_WP_Widget_Blocks_Manager::get_post_id_referenced_in_sidebar( $sidebar_id ); - - $post_id = wp_insert_post( - array( - 'ID' => $id_referenced_in_sidebar, - 'post_content' => $sidebar_content, - 'post_type' => 'wp_area', - ) - ); - - if ( 0 === $id_referenced_in_sidebar ) { - Experimental_WP_Widget_Blocks_Manager::reference_post_id_in_sidebar( $sidebar_id, $post_id ); - } - } -} -add_action( 'customize_update_gutenberg_widget_blocks', 'gutenberg_customize_update', 10, 2 ); - /** * Filters the Customizer widget settings arguments. * This is needed because the Customizer registers settings for the raw registered widgets, without going through the `sidebars_widgets` filter. diff --git a/lib/load.php b/lib/load.php index cc8c54210033..ed97a9c2188d 100644 --- a/lib/load.php +++ b/lib/load.php @@ -29,12 +29,13 @@ function gutenberg_is_experiment_enabled( $name ) { /** * Start: Include for phase 2 */ - if ( ! class_exists( 'WP_REST_Widget_Forms' ) ) { - require dirname( __FILE__ ) . '/class-wp-rest-widget-forms.php'; - } - if ( ! class_exists( 'WP_REST_Widget_Areas_Controller' ) ) { - require dirname( __FILE__ ) . '/class-experimental-wp-widget-blocks-manager.php'; - require dirname( __FILE__ ) . '/class-wp-rest-widget-areas-controller.php'; + if ( gutenberg_is_experiment_enabled( 'gutenberg-widget-experiments' ) ) { + if ( ! class_exists( 'WP_REST_Widget_Utils_Controller' ) ) { + require dirname( __FILE__ ) . '/class-wp-rest-widget-utils-controller.php'; + } + if ( ! class_exists( 'WP_REST_Sidebars_Controller' ) ) { + require_once dirname( __FILE__ ) . '/class-wp-rest-sidebars-controller.php'; + } } if ( ! class_exists( 'WP_REST_Block_Directory_Controller' ) ) { require dirname( __FILE__ ) . '/class-wp-rest-block-directory-controller.php'; @@ -82,6 +83,12 @@ function gutenberg_is_experiment_enabled( $name ) { if ( ! class_exists( 'WP_Block_List' ) ) { require dirname( __FILE__ ) . '/class-wp-block-list.php'; } +if ( gutenberg_is_experiment_enabled( 'gutenberg-widget-experiments' ) ) { + if ( ! class_exists( 'WP_Widget_Block' ) ) { + require_once dirname( __FILE__ ) . '/class-wp-widget-block.php'; + } + require_once dirname( __FILE__ ) . '/widgets-page.php'; +} require dirname( __FILE__ ) . '/compat.php'; require dirname( __FILE__ ) . '/utils.php'; @@ -95,7 +102,6 @@ function gutenberg_is_experiment_enabled( $name ) { require dirname( __FILE__ ) . '/block-directory.php'; require dirname( __FILE__ ) . '/demo.php'; require dirname( __FILE__ ) . '/widgets.php'; -require dirname( __FILE__ ) . '/widgets-page.php'; require dirname( __FILE__ ) . '/navigation-page.php'; require dirname( __FILE__ ) . '/experiments-page.php'; require dirname( __FILE__ ) . '/customizer.php'; diff --git a/lib/rest-api.php b/lib/rest-api.php index 10b7724caf7f..e8f3a2bc7827 100644 --- a/lib/rest-api.php +++ b/lib/rest-api.php @@ -136,22 +136,13 @@ function gutenberg_filter_rest_prepare_theme( $response, $theme, $request ) { * @since 5.0.0 */ function gutenberg_register_rest_widget_updater_routes() { - $widget_forms = new WP_REST_Widget_Forms(); - $widget_forms->register_routes(); + if ( gutenberg_is_experiment_enabled( 'gutenberg-widget-experiments' ) ) { + $widget_forms = new WP_REST_Widget_Utils_Controller(); + $widget_forms->register_routes(); + } } add_action( 'rest_api_init', 'gutenberg_register_rest_widget_updater_routes' ); -/** - * Registers the widget area REST API routes. - * - * @since 5.7.0 - */ -function gutenberg_register_rest_widget_areas() { - $widget_areas_controller = new WP_REST_Widget_Areas_Controller(); - $widget_areas_controller->register_routes(); -} -add_action( 'rest_api_init', 'gutenberg_register_rest_widget_areas' ); - /** * Registers the block directory. * @@ -199,6 +190,17 @@ function gutenberg_register_plugins_endpoint() { } add_action( 'rest_api_init', 'gutenberg_register_plugins_endpoint' ); +/** + * Registers the Sidebars REST API routes. + */ +function gutenberg_register_sidebars_endpoint() { + if ( gutenberg_is_experiment_enabled( 'gutenberg-widget-experiments' ) ) { + $sidebars = new WP_REST_Sidebars_Controller(); + $sidebars->register_routes(); + } +} +add_action( 'rest_api_init', 'gutenberg_register_sidebars_endpoint' ); + /** * Hook in to the nav menu item post type and enable a post type rest endpoint. * diff --git a/lib/widgets-page.php b/lib/widgets-page.php index a6dae5ee90ba..3b6c8e96bb6a 100644 --- a/lib/widgets-page.php +++ b/lib/widgets-page.php @@ -36,8 +36,8 @@ class="blocks-widgets-container * @param string $hook Page. */ function gutenberg_widgets_init( $hook ) { - if ( 'gutenberg_page_gutenberg-widgets' !== $hook && 'gutenberg_customizer' !== $hook ) { - return; + if ( ! in_array( $hook, array( 'gutenberg_page_gutenberg-widgets', 'gutenberg_customizer', 'widgets.php' ), true ) ) { + return; } $initializer_name = 'gutenberg_page_gutenberg-widgets' === $hook diff --git a/lib/widgets.php b/lib/widgets.php index 165d4792dd84..6bc481fe71cc 100644 --- a/lib/widgets.php +++ b/lib/widgets.php @@ -137,6 +137,7 @@ function gutenberg_get_legacy_widget_settings() { foreach ( $wp_widget_factory->widgets as $class => $widget_obj ) { $available_legacy_widgets[ $class ] = array( 'name' => html_entity_decode( $widget_obj->name ), + 'id_base' => $widget_obj->id_base, // wp_widget_description is not being used because its input parameter is a Widget Id. // Widgets id's reference to a specific widget instance. // Here we are iterating on all the available widget classes even if no widget instance exists for them. @@ -243,8 +244,6 @@ function gutenberg_create_wp_area_post_type() { } add_action( 'init', 'gutenberg_create_wp_area_post_type' ); -add_filter( 'sidebars_widgets', 'Experimental_WP_Widget_Blocks_Manager::swap_out_sidebars_blocks_for_block_widgets' ); - /** * Function to enqueue admin-widgets as part of the block editor assets. */ diff --git a/packages/block-library/src/legacy-widget/edit/dom-manager.js b/packages/block-library/src/legacy-widget/edit/dom-manager.js index 80721ceb33e5..ab3647f36230 100644 --- a/packages/block-library/src/legacy-widget/edit/dom-manager.js +++ b/packages/block-library/src/legacy-widget/edit/dom-manager.js @@ -7,7 +7,6 @@ import { includes } from 'lodash'; * WordPress dependencies */ import { Component, createRef } from '@wordpress/element'; -import isShallowEqual from '@wordpress/is-shallow-equal'; class LegacyWidgetEditDomManager extends Component { constructor() { @@ -23,7 +22,6 @@ class LegacyWidgetEditDomManager extends Component { componentDidMount() { this.triggerWidgetEvent( 'widget-added' ); - this.previousFormData = new window.FormData( this.formRef.current ); } shouldComponentUpdate( nextProps ) { @@ -53,7 +51,6 @@ class LegacyWidgetEditDomManager extends Component { } if ( shouldTriggerWidgetUpdateEvent ) { this.triggerWidgetEvent( 'widget-updated' ); - this.previousFormData = new window.FormData( this.formRef.current ); } return false; } @@ -67,22 +64,9 @@ class LegacyWidgetEditDomManager extends Component { className="form" ref={ this.formRef } method="post" - onBlur={ () => { - if ( this.shouldTriggerInstanceUpdate() ) { - if ( isReferenceWidget ) { - if ( this.containerRef.current ) { - window.wpWidgets.save( - window.jQuery( - this.containerRef.current - ) - ); - } - } - this.props.onInstanceChange( - this.retrieveUpdatedInstance() - ); - } - } } + onBlur={ () => + this.props.onInstanceChange( this.getFormData() ) + } >
{ + this.requestWidgetForm( undefined, ( response ) => { this.props.onInstanceChange( null, !! response.form ); } ); } @@ -41,11 +41,8 @@ class LegacyWidgetEditHandler extends Component { if ( ! this.widgetNonce ) { this.trySetNonce(); } - if ( - prevProps.instance !== this.props.instance && - this.instanceUpdating !== this.props.instance - ) { - this.requestWidgetUpdater( undefined, ( response ) => { + if ( prevProps.widgetClass !== this.props.widgetClass ) { + this.requestWidgetForm( undefined, ( response ) => { this.props.onInstanceChange( null, !! response.form ); } ); } @@ -126,20 +123,10 @@ class LegacyWidgetEditHandler extends Component { } onInstanceChange( instanceChanges ) { - const { id } = this.props; - if ( id ) { - // For reference widgets there is no need to query an endpoint, - // the widget is already saved with ajax. - this.props.onInstanceChange( instanceChanges, true ); - return; - } - this.requestWidgetUpdater( instanceChanges, ( response ) => { - this.instanceUpdating = response.instance; - this.props.onInstanceChange( response.instance, !! response.form ); - } ); + this.props.onInstanceChange( instanceChanges, true ); } - requestWidgetUpdater( instanceChanges, callback ) { + requestWidgetForm( updatedInstance, callback ) { const { id, idBase, instance, widgetClass } = this.props; const { isStillMounted } = this; if ( ! id && ! widgetClass ) { @@ -173,12 +160,14 @@ class LegacyWidgetEditHandler extends Component { if ( widgetClass ) { apiFetch( { - path: `/__experimental/widget-forms/${ encodeURIComponent( + path: `/__experimental/widget-utils/form/${ encodeURIComponent( widgetClass ) }/`, data: { - instance, - instance_changes: instanceChanges, + instance: { + ...instance, + ...updatedInstance, + }, }, method: 'POST', } ).then( ( response ) => { diff --git a/packages/block-library/src/legacy-widget/edit/index.js b/packages/block-library/src/legacy-widget/edit/index.js index 12194929461a..fa4eadfaa25e 100644 --- a/packages/block-library/src/legacy-widget/edit/index.js +++ b/packages/block-library/src/legacy-widget/edit/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { get } from 'lodash'; +import { get, omit } from 'lodash'; /** * WordPress dependencies @@ -45,6 +45,7 @@ class LegacyWidgetEdit extends Component { const widgetObject = ( id && availableLegacyWidgets[ id ] ) || ( widgetClass && availableLegacyWidgets[ widgetClass ] ); + if ( ! id && ! widgetClass ) { return ( { - const { isReferenceWidget } = availableLegacyWidgets[ - newWidget - ]; + const { + isReferenceWidget, + id_base: idBase, + } = availableLegacyWidgets[ newWidget ]; setAttributes( { instance: {}, id: isReferenceWidget ? newWidget : undefined, + idBase, widgetClass: isReferenceWidget ? undefined : newWidget, @@ -171,7 +174,7 @@ class LegacyWidgetEdit extends Component { ); } diff --git a/packages/block-library/src/widget-area/edit/index.js b/packages/block-library/src/widget-area/edit/index.js index f25273433dcc..4db095564247 100644 --- a/packages/block-library/src/widget-area/edit/index.js +++ b/packages/block-library/src/widget-area/edit/index.js @@ -19,10 +19,15 @@ export default function WidgetAreaEdit( { ( select ) => select( 'core/block-editor' ).getBlockIndex( clientId ), [ clientId ] ); + return ( - + diff --git a/packages/block-library/src/widget-area/edit/inner-blocks.js b/packages/block-library/src/widget-area/edit/inner-blocks.js index 5e4b8eb31a70..15ca45640a4c 100644 --- a/packages/block-library/src/widget-area/edit/inner-blocks.js +++ b/packages/block-library/src/widget-area/edit/inner-blocks.js @@ -7,7 +7,7 @@ import { InnerBlocks } from '@wordpress/block-editor'; export default function WidgetAreaInnerBlocks() { const [ blocks, onInput, onChange ] = useEntityBlockEditor( 'root', - 'widgetArea' + 'postType' ); return ( +
- + { - return !! select( 'core/interface' ).getActiveComplementaryArea( - 'core/edit-widgets' - ); - } ); + const hasSidebarEnabled = useSelect( + ( select ) => + !! select( 'core/interface' ).getActiveComplementaryArea( + 'core/edit-widgets' + ) + ); return ( ) } - content={ } + content={ + + } /> diff --git a/packages/edit-widgets/src/components/save-button/index.js b/packages/edit-widgets/src/components/save-button/index.js index 637aac055e63..5fe82069abc5 100644 --- a/packages/edit-widgets/src/components/save-button/index.js +++ b/packages/edit-widgets/src/components/save-button/index.js @@ -1,49 +1,34 @@ -/** - * External dependencies - */ -import { filter, map, some, forEach } from 'lodash'; - /** * WordPress dependencies */ import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { useCallback } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; +/** + * Internal dependencies + */ + function SaveButton() { - const { editedWidgetAreaIds, isSaving } = useSelect( ( select ) => { - const { - hasEditsForEntityRecord, - isSavingEntityRecord, - getEntityRecords, - } = select( 'core' ); - const widgetAreas = getEntityRecords( 'root', 'widgetArea' ); - const widgetAreaIds = map( widgetAreas, ( { id } ) => id ); + const { hasEditedWidgetAreaIds, isSaving } = useSelect( ( select ) => { + const { getEditedWidgetAreas, isSavingWidgetAreas } = select( + 'core/edit-widgets' + ); + return { - editedWidgetAreaIds: filter( widgetAreaIds, ( id ) => - hasEditsForEntityRecord( 'root', 'widgetArea', id ) - ), - isSaving: some( widgetAreaIds, ( id ) => - isSavingEntityRecord( 'root', 'widgetArea', id ) - ), + hasEditedWidgetAreaIds: getEditedWidgetAreas()?.length > 0, + isSaving: isSavingWidgetAreas(), }; }, [] ); - const { saveEditedEntityRecord } = useDispatch( 'core' ); - - const onClick = useCallback( () => { - forEach( editedWidgetAreaIds, ( id ) => { - saveEditedEntityRecord( 'root', 'widgetArea', id ); - } ); - }, [ editedWidgetAreaIds ] ); + const { saveEditedWidgetAreas } = useDispatch( 'core/edit-widgets' ); return ( diff --git a/packages/edit-widgets/src/components/widget-areas-block-editor-content/index.js b/packages/edit-widgets/src/components/widget-areas-block-editor-content/index.js index 21e91953e5bf..92f82f5b4325 100644 --- a/packages/edit-widgets/src/components/widget-areas-block-editor-content/index.js +++ b/packages/edit-widgets/src/components/widget-areas-block-editor-content/index.js @@ -3,10 +3,10 @@ */ import { Popover } from '@wordpress/components'; import { + BlockList, BlockEditorKeyboardShortcuts, WritingFlow, ObserveTyping, - BlockList, } from '@wordpress/block-editor'; import { useDispatch } from '@wordpress/data'; @@ -18,6 +18,7 @@ import KeyboardShortcuts from '../keyboard-shortcuts'; export default function WidgetAreasBlockEditorContent() { const { clearSelectedBlock } = useDispatch( 'core/block-editor' ); + return ( <> diff --git a/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js b/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js index ebddb5a6e797..6815470efe24 100644 --- a/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js +++ b/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js @@ -13,8 +13,7 @@ import { } from '@wordpress/components'; import { uploadMedia } from '@wordpress/media-utils'; import { useSelect } from '@wordpress/data'; -import { useEffect, useMemo, useState } from '@wordpress/element'; -import { createBlock } from '@wordpress/blocks'; +import { useMemo } from '@wordpress/element'; import { BlockEditorProvider, BlockEditorKeyboardShortcuts, @@ -24,37 +23,20 @@ import { * Internal dependencies */ import KeyboardShortcuts from '../keyboard-shortcuts'; - -const EMPTY_ARRAY = []; +import { useEntityBlockEditor } from '@wordpress/core-data'; +import { buildWidgetAreasPostId, KIND, POST_TYPE } from '../../store/utils'; export default function WidgetAreasBlockEditorProvider( { blockEditorSettings, ...props } ) { - const { areas, hasUploadPermissions } = useSelect( ( select ) => { - const { canUser, getEntityRecords } = select( 'core' ); - return { - areas: getEntityRecords( 'root', 'widgetArea' ) || EMPTY_ARRAY, - hasUploadPermissions: defaultTo( - canUser( 'create', 'media' ), - true - ), - }; - } ); - const [ blocks, setBlocks ] = useState( [] ); - useEffect( () => { - if ( ! areas || ! areas.length || blocks.length > 0 ) { - return; - } - setBlocks( - areas.map( ( { id, name } ) => { - return createBlock( 'core/widget-area', { - id, - name, - } ); - } ) - ); - }, [ areas, blocks ] ); + const { hasUploadPermissions } = useSelect( ( select ) => ( { + hasUploadPermissions: defaultTo( + select( 'core' ).canUser( 'create', 'media' ), + true + ), + widgetAreas: select( 'core/edit-widgets' ).getWidgetAreas(), + } ) ); const settings = useMemo( () => { let mediaUploadBlockEditor; @@ -74,6 +56,12 @@ export default function WidgetAreasBlockEditorProvider( { }; }, [ blockEditorSettings, hasUploadPermissions ] ); + const [ blocks, onInput, onChange ] = useEntityBlockEditor( + KIND, + POST_TYPE, + { id: buildWidgetAreasPostId() } + ); + return ( <> @@ -83,8 +71,8 @@ export default function WidgetAreasBlockEditorProvider( { setBlocks( newBlocks ) } - onChange={ ( newBlocks ) => setBlocks( newBlocks ) } + onInput={ onInput } + onChange={ onChange } settings={ settings } { ...props } /> diff --git a/packages/edit-widgets/src/index.js b/packages/edit-widgets/src/index.js index ef10dd0f5096..4e5adccb4db2 100644 --- a/packages/edit-widgets/src/index.js +++ b/packages/edit-widgets/src/index.js @@ -11,6 +11,7 @@ import { /** * Internal dependencies */ +import './store'; import './hooks'; import EditWidgetsInitializer from './components/edit-widgets-initializer'; import CustomizerEditWidgetsInitializer from './components/customizer-edit-widgets-initializer'; diff --git a/packages/edit-widgets/src/store/actions.js b/packages/edit-widgets/src/store/actions.js new file mode 100644 index 000000000000..44982e4e84fd --- /dev/null +++ b/packages/edit-widgets/src/store/actions.js @@ -0,0 +1,111 @@ +/** + * External dependencies + */ +import { invert } from 'lodash'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { dispatch, select, getWidgetToClientIdMapping } from './controls'; +import { transformBlockToWidget } from './transformers'; +import { + buildWidgetAreaPostId, + buildWidgetAreasQuery, + KIND, + POST_TYPE, + WIDGET_AREA_ENTITY_TYPE, +} from './utils'; + +export function* saveEditedWidgetAreas() { + const editedWidgetAreas = yield select( + 'core/edit-widgets', + 'getEditedWidgetAreas' + ); + if ( ! editedWidgetAreas?.length ) { + return; + } + try { + yield* saveWidgetAreas( editedWidgetAreas ); + yield dispatch( + 'core/notices', + 'createSuccessNotice', + __( 'Widgets saved.' ), + { + type: 'snackbar', + } + ); + } catch ( e ) { + yield dispatch( + 'core/notices', + 'createErrorNotice', + __( 'There was an error.' ), + { + type: 'snackbar', + } + ); + } +} + +export function* saveWidgetAreas( widgetAreas ) { + const widgets = yield select( 'core/edit-widgets', 'getWidgets' ); + const widgetIdToClientId = yield getWidgetToClientIdMapping(); + const clientIdToWidgetId = invert( widgetIdToClientId ); + + // @TODO: Batch save / concurrency + for ( const widgetArea of widgetAreas ) { + const post = yield select( + 'core', + 'getEditedEntityRecord', + KIND, + POST_TYPE, + buildWidgetAreaPostId( widgetArea.id ) + ); + const widgetsBlocks = post.blocks; + const newWidgets = widgetsBlocks.map( ( block ) => { + const widgetId = clientIdToWidgetId[ block.clientId ]; + const oldWidget = widgets[ widgetId ]; + return transformBlockToWidget( block, oldWidget ); + } ); + + yield dispatch( + 'core', + 'editEntityRecord', + KIND, + WIDGET_AREA_ENTITY_TYPE, + widgetArea.id, + { widgets: newWidgets } + ); + + yield dispatch( + 'core', + 'saveEditedEntityRecord', + KIND, + WIDGET_AREA_ENTITY_TYPE, + widgetArea.id + ); + + yield dispatch( + 'core', + 'receiveEntityRecords', + KIND, + POST_TYPE, + post, + undefined + ); + } + + // saveEditedEntityRecord resets the resolution status, let's fix it manually + yield dispatch( + 'core', + 'finishResolution', + 'getEntityRecord', + KIND, + WIDGET_AREA_ENTITY_TYPE, + buildWidgetAreasQuery() + ); +} diff --git a/packages/edit-widgets/src/store/controls.js b/packages/edit-widgets/src/store/controls.js new file mode 100644 index 000000000000..1b836b5be1aa --- /dev/null +++ b/packages/edit-widgets/src/store/controls.js @@ -0,0 +1,171 @@ +/** + * WordPress dependencies + */ +import { createRegistryControl } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { buildWidgetAreasQuery, KIND, WIDGET_AREA_ENTITY_TYPE } from './utils'; + +/** + * Trigger an API Fetch request. + * + * @param {Object} request API Fetch Request Object. + * @return {Object} control descriptor. + */ +export function apiFetch( request ) { + return { + type: 'API_FETCH', + request, + }; +} + +/** + * Returns a list of pending actions for given post id. + * + * @param {number} postId Post ID. + * @return {Array} List of pending actions. + */ +export function getPendingActions( postId ) { + return { + type: 'GET_PENDING_ACTIONS', + postId, + }; +} + +/** + * Returns boolean indicating whether or not an action processing specified + * post is currently running. + * + * @param {number} postId Post ID. + * @return {Object} Action. + */ +export function isProcessingPost( postId ) { + return { + type: 'IS_PROCESSING_POST', + postId, + }; +} + +/** + * Selects widgetId -> clientId mapping (necessary for saving widgets). + * + * @return {Object} Action. + */ +export function getWidgetToClientIdMapping() { + return { + type: 'GET_WIDGET_TO_CLIENT_ID_MAPPING', + }; +} + +/** + * Resolves navigation post for given menuId. + * + * @see selectors.js + * @param {number} menuId Menu ID. + * @return {Object} Action. + */ +export function getNavigationPostForMenu( menuId ) { + return { + type: 'SELECT', + registryName: 'core/edit-navigation', + selectorName: 'getNavigationPostForMenu', + args: [ menuId ], + }; +} + +/** + * Resolves menu items for given menu id. + * + * @param {Object} query Query. + * @return {Object} Action. + */ +export function resolveWidgetAreas( query = buildWidgetAreasQuery() ) { + return { + type: 'RESOLVE_WIDGET_AREAS', + query, + }; +} + +/** + * Calls a selector using chosen registry. + * + * @param {string} registryName Registry name. + * @param {string} selectorName Selector name. + * @param {Array} args Selector arguments. + * @return {Object} control descriptor. + */ +export function select( registryName, selectorName, ...args ) { + return { + type: 'SELECT', + registryName, + selectorName, + args, + }; +} + +/** + * Dispatches an action using chosen registry. + * + * @param {string} registryName Registry name. + * @param {string} actionName Action name. + * @param {Array} args Selector arguments. + * @return {Object} control descriptor. + */ +export function dispatch( registryName, actionName, ...args ) { + return { + type: 'DISPATCH', + registryName, + actionName, + args, + }; +} + +const controls = { + SELECT: createRegistryControl( + ( registry ) => ( { registryName, selectorName, args } ) => { + return registry.select( registryName )[ selectorName ]( ...args ); + } + ), + + GET_PENDING_ACTIONS: createRegistryControl( + ( registry ) => ( { postId } ) => { + return ( + getState( registry ).processingQueue[ postId ] + ?.pendingActions || [] + ); + } + ), + + IS_PROCESSING_POST: createRegistryControl( + ( registry ) => ( { postId } ) => { + return getState( registry ).processingQueue[ postId ]?.inProgress; + } + ), + + GET_WIDGET_TO_CLIENT_ID_MAPPING: createRegistryControl( + ( registry ) => () => { + return getState( registry ).mapping || {}; + } + ), + + DISPATCH: createRegistryControl( + ( registry ) => ( { registryName, actionName, args } ) => { + return registry.dispatch( registryName )[ actionName ]( ...args ); + } + ), + + RESOLVE_WIDGET_AREAS: createRegistryControl( + ( registry ) => ( { query } ) => { + return registry + .__experimentalResolveSelect( 'core' ) + .getEntityRecords( KIND, WIDGET_AREA_ENTITY_TYPE, query ); + } + ), +}; + +const getState = ( registry ) => + registry.stores[ 'core/edit-widgets' ].store.getState(); + +export default controls; diff --git a/packages/edit-widgets/src/store/index.js b/packages/edit-widgets/src/store/index.js new file mode 100644 index 000000000000..fdd2cdc915fb --- /dev/null +++ b/packages/edit-widgets/src/store/index.js @@ -0,0 +1,37 @@ +/** + * WordPress dependencies + */ +import { registerStore } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import reducer from './reducer'; +import * as resolvers from './resolvers'; +import * as selectors from './selectors'; +import * as actions from './actions'; +import controls from './controls'; + +/** + * Module Constants + */ +const MODULE_KEY = 'core/edit-widgets'; + +/** + * Block editor data store configuration. + * + * @see https://github.com/WordPress/gutenberg/blob/master/packages/data/README.md#registerStore + * + * @type {Object} + */ +export const storeConfig = { + reducer, + controls, + selectors, + resolvers, + actions, +}; + +const store = registerStore( MODULE_KEY, storeConfig ); + +export default store; diff --git a/packages/edit-widgets/src/store/reducer.js b/packages/edit-widgets/src/store/reducer.js new file mode 100644 index 000000000000..7733f7f3a925 --- /dev/null +++ b/packages/edit-widgets/src/store/reducer.js @@ -0,0 +1,26 @@ +/** + * WordPress dependencies + */ +import { combineReducers } from '@wordpress/data'; + +/** + * Internal to edit-widgets package. + * + * Stores widgetId -> clientId mapping which is necessary for saving the navigation. + * + * @param {Object} state Redux state + * @param {Object} action Redux action + * @return {Object} Updated state + */ +export function mapping( state, action ) { + const { type, ...rest } = action; + if ( type === 'SET_WIDGET_TO_CLIENT_ID_MAPPING' ) { + return rest.mapping; + } + + return state || {}; +} + +export default combineReducers( { + mapping, +} ); diff --git a/packages/edit-widgets/src/store/resolvers.js b/packages/edit-widgets/src/store/resolvers.js new file mode 100644 index 000000000000..52cd625c4f3a --- /dev/null +++ b/packages/edit-widgets/src/store/resolvers.js @@ -0,0 +1,89 @@ +/** + * WordPress dependencies + */ +import { createBlock } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { resolveWidgetAreas, select, dispatch } from './controls'; +import { + KIND, + POST_TYPE, + WIDGET_AREA_ENTITY_TYPE, + buildWidgetAreasQuery, + buildWidgetAreaPostId, + buildWidgetAreasPostId, +} from './utils'; +import { transformWidgetToBlock } from './transformers'; + +export function* getWidgetAreas() { + const query = buildWidgetAreasQuery(); + yield resolveWidgetAreas( query ); + const widgetAreas = yield select( + 'core', + 'getEntityRecords', + KIND, + WIDGET_AREA_ENTITY_TYPE, + query + ); + + const widgetAreaBlocks = []; + const widgetIdToClientId = {}; + for ( const widgetArea of widgetAreas ) { + const widgetBlocks = []; + for ( const widget of widgetArea.widgets ) { + const block = transformWidgetToBlock( widget ); + widgetIdToClientId[ widget.id ] = block.clientId; + widgetBlocks.push( block ); + } + + // Persist the actual post containing the navigation block + yield persistStubPost( + buildWidgetAreaPostId( widgetArea.id ), + widgetBlocks + ); + + widgetAreaBlocks.push( + createBlock( 'core/widget-area', { + id: widgetArea.id, + name: widgetArea.name, + } ) + ); + } + + yield persistStubPost( buildWidgetAreasPostId(), widgetAreaBlocks ); + + yield { + type: 'SET_WIDGET_TO_CLIENT_ID_MAPPING', + mapping: widgetIdToClientId, + }; +} + +const persistStubPost = function* ( id, blocks ) { + const stubPost = createStubPost( id, blocks ); + const args = [ KIND, POST_TYPE, id ]; + yield dispatch( 'core', 'startResolution', 'getEntityRecord', args ); + yield dispatch( + 'core', + 'receiveEntityRecords', + KIND, + POST_TYPE, + stubPost, + { id: stubPost.id }, + false + ); + yield dispatch( 'core', 'finishResolution', 'getEntityRecord', args ); + return stubPost; +}; + +const createStubPost = ( id, blocks ) => ( { + id, + slug: id, + status: 'draft', + type: 'page', + blocks, + meta: { + widgetAreaId: id, + }, +} ); diff --git a/packages/edit-widgets/src/store/selectors.js b/packages/edit-widgets/src/store/selectors.js new file mode 100644 index 000000000000..38427217bda3 --- /dev/null +++ b/packages/edit-widgets/src/store/selectors.js @@ -0,0 +1,115 @@ +/** + * External dependencies + */ +import { keyBy } from 'lodash'; + +/** + * WordPress dependencies + */ +import { createRegistrySelector } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { + buildWidgetAreasQuery, + buildWidgetAreaPostId, + KIND, + POST_TYPE, + WIDGET_AREA_ENTITY_TYPE, +} from './utils'; + +export const getWidgets = createRegistrySelector( ( select ) => () => { + const initialWidgetAreas = select( 'core/edit-widgets' ).getWidgetAreas(); + + return keyBy( + initialWidgetAreas.flatMap( ( area ) => area.widgets ), + ( widget ) => widget.id + ); +} ); + +export const getWidgetAreas = createRegistrySelector( ( select ) => () => { + if ( ! hasResolvedWidgetAreas( query ) ) { + return null; + } + + const query = buildWidgetAreasQuery(); + return select( 'core' ) + .getEntityRecords( KIND, WIDGET_AREA_ENTITY_TYPE, query ) + .filter( ( { id } ) => id !== 'wp_inactive_widgets' ); +} ); + +export const getEditedWidgetAreas = createRegistrySelector( + ( select ) => ( state, ids ) => { + let widgetAreas = select( 'core/edit-widgets' ).getWidgetAreas(); + if ( ! widgetAreas ) { + return []; + } + if ( ids ) { + widgetAreas = widgetAreas.filter( ( { id } ) => + ids.includes( id ) + ); + } + return widgetAreas + .filter( ( { id } ) => + select( 'core' ).hasEditsForEntityRecord( + KIND, + POST_TYPE, + buildWidgetAreaPostId( id ) + ) + ) + .map( ( { id } ) => + select( 'core' ).getEditedEntityRecord( + KIND, + WIDGET_AREA_ENTITY_TYPE, + id + ) + ); + } +); + +export const isSavingWidgetAreas = createRegistrySelector( + ( select ) => ( state, ids ) => { + if ( ! ids ) { + ids = select( 'core/edit-widgets' ) + .getWidgetAreas() + ?.map( ( { id } ) => id ); + } + for ( const id in ids ) { + const isSaving = select( 'core' ).isSavingEntityRecord( + KIND, + WIDGET_AREA_ENTITY_TYPE, + id + ); + if ( isSaving ) { + return true; + } + } + return false; + } +); + +/** + * Returns true if the navigation post related to menuId was already resolved. + * + * @param {number} menuId The id of menu. + * @return {boolean} True if the navigation post related to menuId was already resolved, false otherwise. + */ +export const hasResolvedWidgetAreas = createRegistrySelector( + ( select, query = buildWidgetAreasQuery() ) => () => { + const areas = select( 'core' ).getEntityRecords( + KIND, + WIDGET_AREA_ENTITY_TYPE, + query + ); + if ( ! areas?.length ) { + return select( 'core' ).hasFinishedResolution( 'getEntityRecords', [ + KIND, + WIDGET_AREA_ENTITY_TYPE, + query, + ] ); + } + + return true; + } +); diff --git a/packages/edit-widgets/src/store/transformers.js b/packages/edit-widgets/src/store/transformers.js new file mode 100644 index 000000000000..ea855b267eda --- /dev/null +++ b/packages/edit-widgets/src/store/transformers.js @@ -0,0 +1,54 @@ +/** + * WordPress dependencies + */ +import { createBlock, parse, serialize } from '@wordpress/blocks'; + +export function transformWidgetToBlock( widget ) { + if ( widget.widget_class === 'WP_Widget_Block' ) { + const parsedBlocks = parse( widget.settings.content ); + if ( ! parsedBlocks.length ) { + return createBlock( 'core/paragraph', {}, [] ); + } + return parsedBlocks[ 0 ]; + } + + return createBlock( + 'core/legacy-widget', + { + rendered: widget.rendered, + form: widget.form, + id: widget.id, + widgetClass: widget.widget_class, + instance: widget.settings, + idBase: widget.id_base, + number: widget.number, + }, + [] + ); +} + +export function transformBlockToWidget( block, relatedWidget = {} ) { + const { name, attributes } = block; + if ( name === 'core/legacy-widget' ) { + const widget = { + ...relatedWidget, + id: attributes.id, + widget_class: attributes.widgetClass, + number: attributes.number, + id_base: attributes.idBase, + settings: attributes.instance, + }; + delete widget.form; + delete widget.rendered; + return widget; + } + + return { + ...relatedWidget, + id_base: 'block', + widget_class: 'WP_Widget_Block', + settings: { + content: serialize( block ), + }, + }; +} diff --git a/packages/edit-widgets/src/store/utils.js b/packages/edit-widgets/src/store/utils.js new file mode 100644 index 000000000000..ba47f57f8320 --- /dev/null +++ b/packages/edit-widgets/src/store/utils.js @@ -0,0 +1,45 @@ +/** + * "Kind" of the navigation post. + * + * @type {string} + */ +export const KIND = 'root'; + +/** + * "post type" of the navigation post. + * + * @type {string} + */ +export const WIDGET_AREA_ENTITY_TYPE = 'sidebar'; + +/** + * "post type" of the widget area post. + * + * @type {string} + */ +export const POST_TYPE = 'postType'; + +/** + * Builds an ID for a new widget area post. + * + * @param {number} widgetAreaId Widget area id. + * @return {string} An ID. + */ +export const buildWidgetAreaPostId = ( widgetAreaId ) => + `widget-area-${ widgetAreaId }`; + +/** + * Builds an ID for a global widget areas post. + * + * @return {string} An ID. + */ +export const buildWidgetAreasPostId = () => `widget-areas`; + +/** + * Builds a query to resolve sidebars. + * + * @return {Object} Query. + */ +export function buildWidgetAreasQuery() { + return {}; +} diff --git a/phpunit/class-rest-sidebars-controller-test.php b/phpunit/class-rest-sidebars-controller-test.php new file mode 100644 index 000000000000..8aff0d33a4e8 --- /dev/null +++ b/phpunit/class-rest-sidebars-controller-test.php @@ -0,0 +1,410 @@ +register_routes(); + } + ); + } + + /** + * Create fake data before our tests run. + * + * @param WP_UnitTest_Factory $factory Helper that lets us create fake data. + */ + public static function wpSetUpBeforeClass( $factory ) { + self::$admin_id = $factory->user->create( + array( + 'role' => 'administrator', + ) + ); + self::$editor_id = $factory->user->create( + array( + 'role' => 'editor', + ) + ); + 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( + $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( '/__experimental/sidebars', $routes ); + $this->assertArrayHasKey( '/__experimental/sidebars/(?P[\w-]+)', $routes ); + } + + /** + * + */ + public function test_context_param() { + } + + /** + * + */ + public function test_get_items() { + $request = new WP_REST_Request( 'GET', '/__experimental/sidebars' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( array(), $data ); + } + + /** + * + */ + public function test_get_items_basic_sidebar() { + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ) + ); + + $request = new WP_REST_Request( 'GET', '/__experimental/sidebars' ); + $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(), + ), + ), + $data + ); + } + + /** + * + */ + public function test_get_items_active_sidebar_with_widgets() { + $this->setup_widget( + 'widget_rss', + 1, + array( + 'title' => 'RSS test', + ) + ); + $this->setup_widget( + 'widget_text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( 'text-1', 'rss-1' ) + ); + + $request = new WP_REST_Request( 'GET', '/__experimental/sidebars' ); + $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
' . "\n ", + ), + 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 + ); + } + + /** + * + */ + public function test_get_item() { + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ) + ); + + $request = new WP_REST_Request( 'GET', '/__experimental/sidebars/sidebar-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(), + ), + $data + ); + } + + /** + * The test_update_item() method does not exist for sidebar. + */ + public function test_create_item() { + } + + /** + * + */ + public function test_update_item() { + $this->setup_widget( + 'widget_rss', + 1, + array( + 'title' => 'RSS test', + ) + ); + $this->setup_widget( + 'widget_text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_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
' . "\n ", + ), + 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
' . "\n ", + ), + ), + ), + $data + ); + } + + /** + * The test_delete_item() method does not exist for sidebar. + */ + public function test_delete_item() { + } + + /** + * The test_prepare_item() method does not exist for sidebar. + */ + public function test_prepare_item() { + } + + /** + * + */ + public function test_get_item_schema() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'OPTIONS', '/__experimental/sidebars' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 5, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'name', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'status', $properties ); + $this->assertArrayHasKey( 'widgets', $properties ); + } +}