PluginProbe
Gutenberg / 23.6.2
Gutenberg v23.6.2
23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 7.4.0 All 402 releases
gutenberg / lib / experimental / dashboard-widgets / widget-types.php

widget-types.php in Gutenberg 23.6.2, at lib/experimental/dashboard-widgets/widget-types.php

211 lines 6.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Widget Types: server-side registry and REST exposure.
4 *
5 * Hydrates `WP_Widget_Type_Registry` from the build manifest at `init`,
6 * and exposes the registry to the client through the
7 * `/wp/v2/widget-modules` REST endpoint. The JS layer reads the endpoint
8 * via core-data and dynamically imports each widget's render module on
9 * the consumer side.
10 *
11 * @package gutenberg
12 */
13
14 require_once __DIR__ . '/class-wp-widget-type.php';
15 require_once __DIR__ . '/class-wp-widget-type-registry.php';
16 require_once __DIR__ . '/class-wp-rest-widget-modules-controller.php';
17
18 /**
19 * Returns the i18n schema describing which widget metadata fields are
20 * translatable and the gettext context to use for each.
21 *
22 * Read once from widget-i18n.json and memoized for the rest of the request.
23 * Decoded as objects, not associative arrays: that is how
24 * `translate_settings_using_i18n_schema()` tells keyed maps apart from
25 * lists.
26 *
27 * @return object Map of translatable field name to gettext context.
28 */
29 function gutenberg_get_widget_metadata_i18n_schema() {
30 static $i18n_schema = null;
31
32 if ( null === $i18n_schema ) {
33 $schema = wp_json_file_decode( __DIR__ . '/widget-i18n.json' );
34 $i18n_schema = is_object( $schema ) ? $schema : new stdClass();
35 }
36
37 return $i18n_schema;
38 }
39
40 /**
41 * Translates a widget's user-facing metadata strings.
42 *
43 * Runs `title`, `description`, `help`, and `keywords` through the widget
44 * i18n schema using the widget's `textdomain`, leaving every other key
45 * untouched. A no-op when the widget declares no `textdomain`.
46 *
47 * @param array $widget Widget data from the build manifest.
48 * @return array Widget data with its translatable strings localized.
49 */
50 function gutenberg_translate_widget_metadata( $widget ) {
51 $textdomain = $widget['textdomain'] ?? null;
52 if ( ! $textdomain ) {
53 return $widget;
54 }
55
56 $i18n_schema = gutenberg_get_widget_metadata_i18n_schema();
57
58 foreach ( array( 'title', 'description', 'help', 'keywords' ) as $field ) {
59 if ( isset( $widget[ $field ], $i18n_schema->$field ) ) {
60 $widget[ $field ] = translate_settings_using_i18n_schema( $i18n_schema->$field, $widget[ $field ], $textdomain );
61 }
62 }
63
64 return $widget;
65 }
66
67 /**
68 * Constrains a widget help note to its allowed shape: `content` keeps
69 * only `em`/`strong` markup, and links missing a `label` or `href` are
70 * dropped.
71 *
72 * @param array|null $help Help note from the build manifest.
73 * @return array|null Sanitized help note, or null when there is no content.
74 */
75 function gutenberg_sanitize_widget_help( $help ) {
76 if ( ! is_array( $help ) || empty( $help['content'] ) || ! is_string( $help['content'] ) ) {
77 return null;
78 }
79
80 $sanitized = array(
81 'content' => wp_kses(
82 $help['content'],
83 array(
84 'em' => array(),
85 'strong' => array(),
86 )
87 ),
88 );
89
90 if ( ! empty( $help['links'] ) && is_array( $help['links'] ) ) {
91 $links = array();
92 foreach ( $help['links'] as $link ) {
93 if ( is_array( $link ) && ! empty( $link['label'] ) && ! empty( $link['href'] ) ) {
94 $links[] = array(
95 'label' => $link['label'],
96 'href' => $link['href'],
97 );
98 }
99 }
100
101 if ( $links ) {
102 $sanitized['links'] = $links;
103 }
104 }
105
106 return $sanitized;
107 }
108
109 /**
110 * Hydrates the widget type registry from the build manifest.
111 *
112 * Iterates the widgets discovered by the build pipeline (via
113 * `gutenberg_get_registered_widget_modules()`) and registers each one in
114 * `WP_Widget_Type_Registry`. The manifest is the single source of widget
115 * authorship in this codebase; this loop is a deterministic copy of it
116 * into the in-memory registry, with no filters in between.
117 */
118 function gutenberg_register_widget_types() {
119 if ( ! function_exists( 'gutenberg_get_registered_widget_modules' ) ) {
120 return;
121 }
122
123 $registry = WP_Widget_Type_Registry::get_instance();
124
125 foreach ( gutenberg_get_registered_widget_modules() as $widget ) {
126 if ( empty( $widget['name'] ) || $registry->is_registered( $widget['name'] ) ) {
127 continue;
128 }
129
130 $widget = gutenberg_translate_widget_metadata( $widget );
131
132 $registry->register(
133 $widget['name'],
134 array(
135 'render_module' => $widget['render_module'] ?? null,
136 'widget_module' => $widget['widget_module'] ?? null,
137 'presentation' => $widget['presentation'] ?? null,
138 'category' => $widget['category'] ?? null,
139 'title' => $widget['title'] ?? null,
140 'description' => $widget['description'] ?? null,
141 'help' => gutenberg_sanitize_widget_help( $widget['help'] ?? null ),
142 'keywords' => $widget['keywords'] ?? null,
143 )
144 );
145 }
146 }
147
148 if ( did_action( 'init' ) ) {
149 gutenberg_register_widget_types();
150 } else {
151 add_action( 'init', 'gutenberg_register_widget_types' );
152 }
153
154 /**
155 * Returns all widget types registered in the widget type registry.
156 *
157 * Convenience accessor around `WP_Widget_Type_Registry::get_all_registered()`
158 * for callers that prefer a function-based API.
159 *
160 * @return WP_Widget_Type[] Associative array of `$name => $widget_type`
161 * pairs.
162 */
163 function gutenberg_get_registered_widget_types() {
164 return WP_Widget_Type_Registry::get_instance()->get_all_registered();
165 }
166
167 /**
168 * Registers the REST controller that exposes the widget type registry.
169 */
170 function gutenberg_register_widget_modules_rest_controller() {
171 $controller = new WP_REST_Widget_Modules_Controller();
172 $controller->register_routes();
173 }
174 add_action( 'rest_api_init', 'gutenberg_register_widget_modules_rest_controller' );
175
176 /**
177 * Adds the registered widget modules to the dashboard page's boot
178 * dependencies.
179 *
180 * The wp-build page templates expose a generic
181 * `{page-id}-wp-admin_boot_dependencies` filter. The dashboard hooks
182 * it to make every registered widget render and metadata module
183 * available in the page's import map for dynamic `import()` calls.
184 *
185 * Both the render module and the metadata module are added as
186 * 'dynamic' dependencies so they are reachable from the import map but
187 * not eagerly executed.
188 *
189 * @param array $boot_dependencies Boot dependencies for the page.
190 * @return array Updated boot dependencies.
191 */
192 function gutenberg_add_widget_modules_to_dashboard_boot_deps( $boot_dependencies ) {
193 foreach ( gutenberg_get_registered_widget_types() as $widget_type ) {
194 if ( $widget_type->render_module ) {
195 $boot_dependencies[] = array(
196 'import' => 'dynamic',
197 'id' => $widget_type->render_module,
198 );
199 }
200 if ( $widget_type->widget_module ) {
201 $boot_dependencies[] = array(
202 'import' => 'dynamic',
203 'id' => $widget_type->widget_module,
204 );
205 }
206 }
207
208 return $boot_dependencies;
209 }
210 add_filter( 'dashboard-wp-admin_boot_dependencies', 'gutenberg_add_widget_modules_to_dashboard_boot_deps' );
211