Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
<?php
/**
* Widget Modules REST API: WP_REST_Widget_Modules_Controller class.
*
* @package gutenberg
*/

if ( ! class_exists( 'WP_REST_Widget_Modules_Controller' ) ) {

/**
* Internal REST controller exposing the widget type registry.
*
* Reads from `WP_Widget_Type_Registry`. Read-only collection and item
* endpoints. Render and encode endpoints are intentionally absent:
* consumers import the render module on the client and render in JS,
* so there is no server-rendered HTML to expose.
*
* The endpoint lives at `/wp/v2/widget-modules` because the entity
* `(kind: 'root', name: 'widgetType')` and the path
* `/wp/v2/widget-types` are already taken by the legacy widgets API.
*/
class WP_REST_Widget_Modules_Controller extends WP_REST_Controller {

/**
* Constructor.
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'widget-modules';
}

/**
* Registers the widget module routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);

register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[a-z0-9-]+\/[a-z0-9-]+)',
array(
'args' => array(
'id' => array(
'description' => __( 'Widget module name including namespace.', 'gutenberg' ),
'type' => 'string',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}

/**
* Checks whether a given request has permission to read widget
* modules.
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error
* otherwise.
*/
public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return $this->check_read_permission();
}

/**
* Checks whether a given request has permission to read a single
* widget module.
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error
* otherwise.
*/
public function get_item_permissions_check( $request ) {
$check = $this->check_read_permission();
if ( is_wp_error( $check ) ) {
return $check;
}

$widget_type = WP_Widget_Type_Registry::get_instance()->get_registered( $request['id'] );
if ( null === $widget_type ) {
return new WP_Error(
'rest_widget_module_invalid',
__( 'Invalid widget module name.', 'gutenberg' ),
array( 'status' => 404 )
);
}

return true;
}

/**
* Verifies the user has the basic read capability.
*
* Widget modules are not sensitive data; they describe what is
* available to render. Gating at the same level as the dashboard
* page menu (which requires `read`) keeps the surface consistent.
*
* @return true|WP_Error True if the request is allowed, WP_Error
* otherwise.
*/
protected function check_read_permission() {
if ( ! current_user_can( 'read' ) ) {
return new WP_Error(
'rest_cannot_view_widget_modules',
__( 'Sorry, you are not allowed to view widget modules.', 'gutenberg' ),
array( 'status' => rest_authorization_required_code() )
);
}

return true;
}

/**
* Retrieves the list of all registered widget modules.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response Response object on success.
*/
public function get_items( $request ) {
$registered = WP_Widget_Type_Registry::get_instance()->get_all_registered();
$data = array();

foreach ( $registered as $widget_type ) {
$item = $this->prepare_item_for_response( $widget_type, $request );
$data[] = $this->prepare_response_for_collection( $item );
}

return rest_ensure_response( $data );
}

/**
* Retrieves a single widget module from the collection.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or
* WP_Error on failure.
*/
public function get_item( $request ) {
$widget_type = WP_Widget_Type_Registry::get_instance()->get_registered( $request['id'] );
if ( null === $widget_type ) {
return new WP_Error(
'rest_widget_module_invalid',
__( 'Invalid widget module name.', 'gutenberg' ),
array( 'status' => 404 )
);
}

return rest_ensure_response( $this->prepare_item_for_response( $widget_type, $request ) );
}

/**
* Prepares a widget type object for serialization.
*
* @param WP_Widget_Type $item Widget type instance.
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response Response object containing the serialized
* widget module data.
*/
public function prepare_item_for_response( $item, $request ) {
$widget_type = $item;
$fields = $this->get_fields_for_response( $request );
$data = array();

if ( rest_is_field_included( 'name', $fields ) ) {
$data['name'] = $widget_type->name;
}
if ( rest_is_field_included( 'render_module', $fields ) ) {
$data['render_module'] = $widget_type->render_module;
}
if ( rest_is_field_included( 'widget_module', $fields ) ) {
$data['widget_module'] = $widget_type->widget_module;
}

$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );

return rest_ensure_response( $data );
}

/**
* Retrieves the widget module 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' => 'widget-module',
'type' => 'object',
'properties' => array(
'name' => array(
'description' => __( 'Widget module name including namespace.', 'gutenberg' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'render_module' => array(
'description' => __( 'Script-module handle for the widget render entry point.', 'gutenberg' ),
'type' => array( 'string', 'null' ),
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'widget_module' => array(
'description' => __( 'Script-module handle for the widget metadata entry point.', 'gutenberg' ),
'type' => array( 'string', 'null' ),
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
),
);

$this->schema = $schema;

return $this->add_additional_fields_schema( $this->schema );
}
}
}
85 changes: 45 additions & 40 deletions lib/experimental/dashboard-widgets/widget-types.php
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
<?php
/**
* Widget Types: server-side registry and client bootstrap.
* Widget Types: server-side registry and REST exposure.
*
* Hydrates `WP_Widget_Type_Registry` from the build manifest at `init`, and
* exposes the registered widget types to the dashboard client through a
* temporary inline global (`window.__registeredWidgetTypes`). The inline
* transport is a stopgap until a REST endpoint replaces it.
* Hydrates `WP_Widget_Type_Registry` from the build manifest at `init`,
* and exposes the registry to the client through the
* `/wp/v2/widget-modules` REST endpoint. The JS layer reads the endpoint
* via core-data and dynamically imports each widget's render module on
* the consumer side.
*
* @package gutenberg
*/

require_once __DIR__ . '/class-wp-widget-type.php';
require_once __DIR__ . '/class-wp-widget-type-registry.php';
require_once __DIR__ . '/class-wp-rest-widget-modules-controller.php';

/**
* Hydrates the widget type registry from the build manifest.
Expand Down Expand Up @@ -64,43 +66,46 @@ function gutenberg_get_registered_widget_types() {
}

/**
* Prints the inline script that exposes the widget types as a global.
*
* Temporary bridge: emits `window.__registeredWidgetTypes` so the
* `core/widget-types` data store has data on first paint. Globals on
* `window` are not the desired surface; this is a stopgap until a REST
* endpoint backed by the widget type registry takes over and the resolver
* fetches via `apiFetch`. Consumers should read widget types through the
* `core/widget-types` data store, not by reaching into the global directly.
* Registers the REST controller that exposes the widget type registry.
*/
function gutenberg_print_widget_types_bootstrap() {
$widget_types = gutenberg_get_registered_widget_types();
if ( empty( $widget_types ) ) {
return;
}

$entries = array_values(
array_map(
static function ( $widget_type ) {
return array_filter(
array(
'name' => $widget_type->name,
'render_module' => $widget_type->render_module,
'widget_module' => $widget_type->widget_module,
)
);
},
$widget_types
)
);
function gutenberg_register_widget_modules_rest_controller() {
$controller = new WP_REST_Widget_Modules_Controller();
$controller->register_routes();
}
add_action( 'rest_api_init', 'gutenberg_register_widget_modules_rest_controller' );

if ( empty( $entries ) ) {
return;
/**
* Adds the registered widget modules to the dashboard page's boot
* dependencies.
*
* The wp-build page templates expose a generic
* `{page-id}-wp-admin_boot_dependencies` filter. The dashboard surface
* hooks it to make every registered widget render and metadata module
* available in the page's import map for dynamic `import()` calls.
*
* Both the render module and the metadata module are added as
* 'dynamic' dependencies so they are reachable from the import map but
* not eagerly executed.
*
* @param array $boot_dependencies Boot dependencies for the page.
* @return array Updated boot dependencies.
*/
function gutenberg_add_widget_modules_to_dashboard_boot_deps( $boot_dependencies ) {
foreach ( gutenberg_get_registered_widget_types() as $widget_type ) {
if ( $widget_type->render_module ) {
$boot_dependencies[] = array(
'import' => 'dynamic',
'id' => $widget_type->render_module,
);
}
if ( $widget_type->widget_module ) {
$boot_dependencies[] = array(
'import' => 'dynamic',
'id' => $widget_type->widget_module,
);
}
}

wp_print_inline_script_tag(
'window.__registeredWidgetTypes = ' . wp_json_encode( $entries ) . ';'
);
return $boot_dependencies;
}

add_action( 'admin_print_scripts', 'gutenberg_print_widget_types_bootstrap' );
add_filter( 'dashboard-wp-admin_boot_dependencies', 'gutenberg_add_widget_modules_to_dashboard_boot_deps' );
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions packages/wp-build/templates/page-wp-admin.php.template
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,21 @@ function {{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_enqueue_scripts( $hook_suf
}
}

/**
* Filters the boot script-module dependencies for the
* {{PAGE_SLUG}}-wp-admin page.
*
* Surfaces extending this page can append entries to the boot
* dependency list. Each entry is an array with 'import' (string
* 'static' or 'dynamic') and 'id' (script-module handle) keys.
*
* @param array $boot_dependencies Boot dependencies for the page.
*/
$boot_dependencies = apply_filters(
'{{PAGE_SLUG}}-wp-admin_boot_dependencies',
$boot_dependencies
);

// Dummy script module to ensure dependencies are loaded
wp_register_script_module(
'{{PAGE_SLUG}}-wp-admin',
Expand Down
15 changes: 15 additions & 0 deletions packages/wp-build/templates/page.php.template
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,21 @@ function {{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_render_page() {
}
}

/**
* Filters the boot script-module dependencies for the
* {{PAGE_SLUG}} page.
*
* Surfaces extending this page can append entries to the boot
* dependency list. Each entry is an array with 'import' (string
* 'static' or 'dynamic') and 'id' (script-module handle) keys.
*
* @param array $boot_dependencies Boot dependencies for the page.
*/
$boot_dependencies = apply_filters(
'{{PAGE_SLUG}}_boot_dependencies',
$boot_dependencies
);

// Dummy script module to ensure dependencies are loaded
wp_register_script_module(
'{{PAGE_SLUG}}',
Expand Down
Loading
Loading