PluginProbe
Gutenberg / 12.1.0
Gutenberg v12.1.0
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 / class-wp-rest-widget-types-controller.php

class-wp-rest-widget-types-controller.php in Gutenberg 12.1.0, at lib/class-wp-rest-widget-types-controller.php

552 lines 15.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * REST API: WP_REST_Widget_Types_Controller class
4 *
5 * @package WordPress
6 * @subpackage REST_API
7 * @since 5.6.0
8 */
9
10 /**
11 * Core class to access widget types via the REST API.
12 *
13 * @since 5.6.0
14 *
15 * @see WP_REST_Controller
16 */
17 class WP_REST_Widget_Types_Controller extends WP_REST_Controller {
18
19 /**
20 * Constructor.
21 *
22 * @since 5.6.0
23 */
24 public function __construct() {
25 $this->namespace = 'wp/v2';
26 $this->rest_base = 'widget-types';
27 }
28
29 /**
30 * Registers the widget type routes.
31 *
32 * @since 5.6.0
33 *
34 * @see register_rest_route()
35 */
36 public function register_routes() {
37 register_rest_route(
38 $this->namespace,
39 '/' . $this->rest_base,
40 array(
41 array(
42 'methods' => WP_REST_Server::READABLE,
43 'callback' => array( $this, 'get_items' ),
44 'permission_callback' => array( $this, 'get_items_permissions_check' ),
45 'args' => $this->get_collection_params(),
46 ),
47 'schema' => array( $this, 'get_public_item_schema' ),
48 )
49 );
50
51 register_rest_route(
52 $this->namespace,
53 '/' . $this->rest_base . '/(?P<id>[a-zA-Z0-9_-]+)',
54 array(
55 'args' => array(
56 'id' => array(
57 'description' => __( 'The widget type id.', 'gutenberg' ),
58 'type' => 'string',
59 ),
60 ),
61 array(
62 'methods' => WP_REST_Server::READABLE,
63 'callback' => array( $this, 'get_item' ),
64 'permission_callback' => array( $this, 'get_item_permissions_check' ),
65 'args' => $this->get_collection_params(),
66 ),
67 'schema' => array( $this, 'get_public_item_schema' ),
68 )
69 );
70
71 register_rest_route(
72 $this->namespace,
73 '/' . $this->rest_base . '/(?P<id>[a-zA-Z0-9_-]+)/encode',
74 array(
75 'args' => array(
76 'id' => array(
77 'description' => __( 'The widget type id.', 'gutenberg' ),
78 'type' => 'string',
79 'required' => true,
80 ),
81 'instance' => array(
82 'description' => __( 'Current instance settings of the widget.', 'gutenberg' ),
83 'type' => 'object',
84 ),
85 'form_data' => array(
86 'description' => __( 'Serialized widget form data to encode into instance settings.', 'gutenberg' ),
87 'type' => 'string',
88 'sanitize_callback' => function( $string ) {
89 $array = array();
90 wp_parse_str( $string, $array );
91 return $array;
92 },
93 ),
94 ),
95 array(
96 'methods' => WP_REST_Server::CREATABLE,
97 'permission_callback' => array( $this, 'get_item_permissions_check' ),
98 'callback' => array( $this, 'encode_form_data' ),
99 ),
100 )
101 );
102 }
103
104 /**
105 * Checks whether a given request has permission to read widget types.
106 *
107 * @since 5.6.0
108 *
109 * @param WP_REST_Request $request Full details about the request.
110 * @return WP_Error|bool True if the request has read access, WP_Error object otherwise.
111 */
112 public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
113 return $this->check_read_permission();
114 }
115
116 /**
117 * Retrieves the list of all widget types.
118 *
119 * @since 5.6.0
120 *
121 * @param WP_REST_Request $request Full details about the request.
122 * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
123 */
124 public function get_items( $request ) {
125 $data = array();
126 foreach ( $this->get_widgets() as $widget ) {
127 $widget_type = $this->prepare_item_for_response( $widget, $request );
128 $data[] = $this->prepare_response_for_collection( $widget_type );
129 }
130
131 return rest_ensure_response( $data );
132 }
133
134 /**
135 * Checks if a given request has access to read a widget type.
136 *
137 * @since 5.6.0
138 *
139 * @param WP_REST_Request $request Full details about the request.
140 * @return WP_Error|bool True if the request has read access for the item, WP_Error object otherwise.
141 */
142 public function get_item_permissions_check( $request ) {
143 $check = $this->check_read_permission();
144 if ( is_wp_error( $check ) ) {
145 return $check;
146 }
147 $widget_id = $request['id'];
148 $widget_type = $this->get_widget( $widget_id );
149 if ( is_wp_error( $widget_type ) ) {
150 return $widget_type;
151 }
152
153 return true;
154 }
155
156 /**
157 * Checks whether the user can read widget types.
158 *
159 * @since 5.6.0
160 *
161 * @return WP_Error|bool True if the widget type is visible, WP_Error otherwise.
162 */
163 protected function check_read_permission() {
164 if ( ! current_user_can( 'edit_theme_options' ) ) {
165 return new WP_Error(
166 'rest_cannot_manage_widgets',
167 __( 'Sorry, you are not allowed to manage widgets on this site.', 'gutenberg' ),
168 array(
169 'status' => rest_authorization_required_code(),
170 )
171 );
172 }
173
174 return true;
175 }
176
177 /**
178 * Gets the details about the requested widget.
179 *
180 * @since 5.6.0
181 *
182 * @param string $id The widget type id.
183 * @return array|WP_Error The array of widget data if the name is valid, WP_Error otherwise.
184 */
185 public function get_widget( $id ) {
186 foreach ( $this->get_widgets() as $widget ) {
187 if ( $id === $widget['id'] ) {
188 return $widget;
189 }
190 }
191
192 return new WP_Error( 'rest_widget_type_invalid', __( 'Invalid widget type.', 'gutenberg' ), array( 'status' => 404 ) );
193 }
194
195 /**
196 * Normalize array of widgets.
197 *
198 * @since 5.6.0
199 *
200 * @global array $wp_registered_widgets The list of registered widgets.
201 *
202 * @return array Array of widgets.
203 */
204 protected function get_widgets() {
205 global $wp_registered_widgets;
206
207 $widgets = array();
208
209 foreach ( $wp_registered_widgets as $widget ) {
210 $parsed_id = wp_parse_widget_id( $widget['id'] );
211 $widget_object = gutenberg_get_widget_object( $parsed_id['id_base'] );
212
213 $widget['id'] = $parsed_id['id_base'];
214 $widget['is_multi'] = (bool) $widget_object;
215 $widget['name'] = html_entity_decode( $widget['name'] );
216 $widget['description'] = html_entity_decode( $widget['description'] );
217
218 unset( $widget['callback'] );
219
220 $classname = '';
221 foreach ( (array) $widget['classname'] as $cn ) {
222 if ( is_string( $cn ) ) {
223 $classname .= '_' . $cn;
224 } elseif ( is_object( $cn ) ) {
225 $classname .= '_' . get_class( $cn );
226 }
227 }
228 $widget['classname'] = ltrim( $classname, '_' );
229
230 $widgets[] = $widget;
231 }
232
233 return $widgets;
234 }
235
236 /**
237 * Retrieves a single widget type from the collection.
238 *
239 * @since 5.6.0
240 *
241 * @param WP_REST_Request $request Full details about the request.
242 * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
243 */
244 public function get_item( $request ) {
245 $widget_id = $request['id'];
246 $widget_type = $this->get_widget( $widget_id );
247 if ( is_wp_error( $widget_type ) ) {
248 return $widget_type;
249 }
250 $data = $this->prepare_item_for_response( $widget_type, $request );
251
252 return rest_ensure_response( $data );
253 }
254
255 /**
256 * Prepares a widget type object for serialization.
257 *
258 * @since 5.6.0
259 *
260 * @param array $widget_type Widget type data.
261 * @param WP_REST_Request $request Full details about the request.
262 * @return WP_REST_Response Widget type data.
263 */
264 public function prepare_item_for_response( $widget_type, $request ) {
265 $fields = $this->get_fields_for_response( $request );
266 $data = array(
267 'id' => $widget_type['id'],
268 );
269
270 $schema = $this->get_item_schema();
271 $extra_fields = array(
272 'name',
273 'description',
274 'is_multi',
275 'classname',
276 'widget_class',
277 'option_name',
278 'customize_selective_refresh',
279 );
280
281 foreach ( $extra_fields as $extra_field ) {
282 if ( ! rest_is_field_included( $extra_field, $fields ) ) {
283 continue;
284 }
285
286 if ( isset( $widget_type[ $extra_field ] ) ) {
287 $field = $widget_type[ $extra_field ];
288 } elseif ( array_key_exists( 'default', $schema['properties'][ $extra_field ] ) ) {
289 $field = $schema['properties'][ $extra_field ]['default'];
290 } else {
291 $field = '';
292 }
293
294 $data[ $extra_field ] = rest_sanitize_value_from_schema( $field, $schema['properties'][ $extra_field ] );
295 }
296
297 $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
298 $data = $this->add_additional_fields_to_object( $data, $request );
299 $data = $this->filter_response_by_context( $data, $context );
300
301 $response = rest_ensure_response( $data );
302
303 $response->add_links( $this->prepare_links( $widget_type ) );
304
305 /**
306 * Filters the REST API response for a widget type.
307 *
308 * @since 5.6.0
309 *
310 * @param WP_REST_Response $response The response object.
311 * @param array $widget_type The array of widget data.
312 * @param WP_REST_Request $request The request object.
313 */
314 return apply_filters( 'rest_prepare_widget_type', $response, $widget_type, $request );
315 }
316
317 /**
318 * Prepares links for the widget type.
319 *
320 * @since 5.6.0
321 *
322 * @param array $widget_type Widget type data.
323 * @return array Links for the given widget type.
324 */
325 protected function prepare_links( $widget_type ) {
326 return array(
327 'collection' => array(
328 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
329 ),
330 'self' => array(
331 'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $widget_type['id'] ) ),
332 ),
333 );
334 }
335
336 /**
337 * Retrieves the widget type's schema, conforming to JSON Schema.
338 *
339 * @since 5.6.0
340 *
341 * @return array Item schema data.
342 */
343 public function get_item_schema() {
344 if ( $this->schema ) {
345 return $this->add_additional_fields_schema( $this->schema );
346 }
347
348 $schema = array(
349 '$schema' => 'http://json-schema.org/draft-04/schema#',
350 'title' => 'widget-type',
351 'type' => 'object',
352 'properties' => array(
353 'id' => array(
354 'description' => __( 'Unique slug identifying the widget type.', 'gutenberg' ),
355 'type' => 'string',
356 'context' => array( 'embed', 'view', 'edit' ),
357 'readonly' => true,
358 ),
359 'name' => array(
360 'description' => __( 'Human-readable name identifying the widget type.', 'gutenberg' ),
361 'type' => 'string',
362 'default' => '',
363 'context' => array( 'embed', 'view', 'edit' ),
364 'readonly' => true,
365 ),
366 'description' => array(
367 'description' => __( 'Description of the widget.', 'gutenberg' ),
368 'type' => 'string',
369 'default' => '',
370 'context' => array( 'view', 'edit', 'embed' ),
371 ),
372 'is_multi' => array(
373 'description' => __( 'Whether the widget supports multiple instances', 'gutenberg' ),
374 'type' => 'boolean',
375 'context' => array( 'view', 'edit', 'embed' ),
376 'readonly' => true,
377 ),
378 'classname' => array(
379 'description' => __( 'Class name', 'gutenberg' ),
380 'type' => 'string',
381 'default' => '',
382 'context' => array( 'embed', 'view', 'edit' ),
383 'readonly' => true,
384 ),
385 ),
386 );
387
388 $this->schema = $schema;
389
390 return $this->add_additional_fields_schema( $this->schema );
391 }
392
393 /**
394 * An RPC-style endpoint which can be used by clients to turn user input in
395 * a widget admin form into an encoded instance object.
396 *
397 * Accepts:
398 *
399 * - id: A widget type ID.
400 * - instance: A widget's encoded instance object. Optional.
401 * - form_data: Form data from submitting a widget's admin form. Optional.
402 *
403 * Returns:
404 * - instance: The encoded instance object after updating the widget with
405 * the given form data.
406 * - form: The widget's admin form after updating the widget with the
407 * given form data.
408 *
409 * @param WP_REST_Request $request Full details about the request.
410 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
411 */
412 public function encode_form_data( $request ) {
413 $id = $request['id'];
414 $widget_object = gutenberg_get_widget_object( $id );
415
416 if ( ! $widget_object ) {
417 return new WP_Error(
418 'rest_invalid_widget',
419 __( 'Cannot preview a widget that does not extend WP_Widget.', 'gutenberg' ),
420 array( 'status' => 400 )
421 );
422 }
423
424 // Set the widget's number so that the id attributes in the HTML that we
425 // return are predictable.
426 if ( isset( $request['number'] ) && is_numeric( $request['number'] ) ) {
427 $widget_object->_set( (int) $request['number'] );
428 } else {
429 $widget_object->_set( -1 );
430 }
431
432 if ( isset( $request['instance']['encoded'], $request['instance']['hash'] ) ) {
433 $serialized_instance = base64_decode( $request['instance']['encoded'] );
434 if ( ! hash_equals( wp_hash( $serialized_instance ), $request['instance']['hash'] ) ) {
435 return new WP_Error(
436 'rest_invalid_widget',
437 __( 'The provided instance is malformed.', 'gutenberg' ),
438 array( 'status' => 400 )
439 );
440 }
441 $instance = unserialize( $serialized_instance );
442 } else {
443 $instance = array();
444 }
445
446 if (
447 isset( $request['form_data'][ "widget-$id" ] ) &&
448 is_array( $request['form_data'][ "widget-$id" ] )
449 ) {
450 $new_instance = array_values( $request['form_data'][ "widget-$id" ] )[0];
451 $old_instance = $instance;
452
453 $instance = $widget_object->update( $new_instance, $old_instance );
454
455 /** This filter is documented in wp-includes/class-wp-widget.php */
456 $instance = apply_filters(
457 'widget_update_callback',
458 $instance,
459 $new_instance,
460 $old_instance,
461 $widget_object
462 );
463 }
464
465 $serialized_instance = serialize( $instance );
466
467 $response = array(
468 'form' => trim(
469 $this->get_widget_form(
470 $widget_object,
471 $instance
472 )
473 ),
474 'preview' => trim(
475 $this->get_widget_preview(
476 $widget_object,
477 $instance
478 )
479 ),
480 'instance' => array(
481 'encoded' => base64_encode( $serialized_instance ),
482 'hash' => wp_hash( $serialized_instance ),
483 ),
484 );
485
486 if ( ! empty( $widget_object->show_instance_in_rest ) ) {
487 // Use new stdClass so that JSON result is {} and not [].
488 $response['instance']['raw'] = empty( $instance ) ? new stdClass : $instance;
489 }
490
491 return rest_ensure_response( $response );
492 }
493
494 /**
495 * Returns the output of WP_Widget::widget() when called with the provided
496 * instance. Used by encode_form_data() to preview a widget.
497
498 * @param WP_Widget $widget_object Widget object to call widget() on.
499 * @param array $instance Widget instance settings.
500 * @return string
501 */
502 private function get_widget_preview( $widget_object, $instance ) {
503 ob_start();
504 the_widget( get_class( $widget_object ), $instance );
505 return ob_get_clean();
506 }
507
508 /**
509 * Returns the output of WP_Widget::form() when called with the provided
510 * instance. Used by encode_form_data() to preview a widget's form.
511 *
512 * @param WP_Widget $widget_object Widget object to call widget() on.
513 * @param array $instance Widget instance settings.
514 * @return string
515 */
516 private function get_widget_form( $widget_object, $instance ) {
517 ob_start();
518
519 /** This filter is documented in wp-includes/class-wp-widget.php */
520 $instance = apply_filters(
521 'widget_form_callback',
522 $instance,
523 $widget_object
524 );
525
526 if ( false !== $instance ) {
527 $return = $widget_object->form( $instance );
528
529 /** This filter is documented in wp-includes/class-wp-widget.php */
530 do_action_ref_array(
531 'in_widget_form',
532 array( &$widget_object, &$return, $instance )
533 );
534 }
535
536 return ob_get_clean();
537 }
538
539 /**
540 * Retrieves the query params for collections.
541 *
542 * @since 5.6.0
543 *
544 * @return array Collection parameters.
545 */
546 public function get_collection_params() {
547 return array(
548 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
549 );
550 }
551 }
552