PluginProbe
Gutenberg / 10.6.1
Gutenberg v10.6.1
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-widgets-controller.php

class-wp-rest-widgets-controller.php in Gutenberg 10.6.1, at lib/class-wp-rest-widgets-controller.php

692 lines 20.8 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_Widgets_Controller class
4 *
5 * @package WordPress
6 * @subpackage REST_API
7 * @since 5.6.0
8 */
9
10 /**
11 * Core class to access widgets via the REST API.
12 *
13 * @since 5.6.0
14 *
15 * @see WP_REST_Controller
16 */
17 class WP_REST_Widgets_Controller extends WP_REST_Controller {
18
19 /**
20 * Widgets controller constructor.
21 *
22 * @since 5.6.0
23 */
24 public function __construct() {
25 $this->namespace = 'wp/v2';
26 $this->rest_base = 'widgets';
27 }
28
29 /**
30 * Registers the widget routes for the controller.
31 *
32 * @since 5.6.0
33 */
34 public function register_routes() {
35 register_rest_route(
36 $this->namespace,
37 $this->rest_base,
38 array(
39 array(
40 'methods' => WP_REST_Server::READABLE,
41 'callback' => array( $this, 'get_items' ),
42 'permission_callback' => array( $this, 'get_items_permissions_check' ),
43 'args' => $this->get_collection_params(),
44 ),
45 array(
46 'methods' => WP_REST_Server::CREATABLE,
47 'callback' => array( $this, 'create_item' ),
48 'permission_callback' => array( $this, 'create_item_permissions_check' ),
49 'args' => $this->get_endpoint_args_for_item_schema(),
50 ),
51 'allow_batch' => array( 'v1' => true ),
52 'schema' => array( $this, 'get_public_item_schema' ),
53 )
54 );
55
56 register_rest_route(
57 $this->namespace,
58 $this->rest_base . '/(?P<id>[\w\-]+)',
59 array(
60 array(
61 'methods' => WP_REST_Server::READABLE,
62 'callback' => array( $this, 'get_item' ),
63 'permission_callback' => array( $this, 'get_item_permissions_check' ),
64 'args' => array(
65 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
66 ),
67 ),
68 array(
69 'methods' => WP_REST_Server::EDITABLE,
70 'callback' => array( $this, 'update_item' ),
71 'permission_callback' => array( $this, 'update_item_permissions_check' ),
72 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
73 ),
74 array(
75 'methods' => WP_REST_Server::DELETABLE,
76 'callback' => array( $this, 'delete_item' ),
77 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
78 'args' => array(
79 'force' => array(
80 'description' => __( 'Whether to force removal of the widget, or move it to the inactive sidebar.', 'gutenberg' ),
81 'type' => 'boolean',
82 ),
83 ),
84 ),
85 'allow_batch' => array( 'v1' => true ),
86 'schema' => array( $this, 'get_public_item_schema' ),
87 )
88 );
89 }
90
91 /**
92 * Checks if a given request has access to get widgets.
93 *
94 * @since 5.6.0
95 *
96 * @param WP_REST_Request $request Full details about the request.
97 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
98 */
99 public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
100 return $this->permissions_check();
101 }
102
103 /**
104 * Retrieves a collection of widgets.
105 *
106 * @since 5.6.0
107 *
108 * @param WP_REST_Request $request Full details about the request.
109 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
110 */
111 public function get_items( $request ) {
112 $prepared = array();
113
114 foreach ( wp_get_sidebars_widgets() as $sidebar_id => $widget_ids ) {
115 if ( isset( $request['sidebar'] ) && $sidebar_id !== $request['sidebar'] ) {
116 continue;
117 }
118
119 foreach ( $widget_ids as $widget_id ) {
120 $response = $this->prepare_item_for_response( compact( 'sidebar_id', 'widget_id' ), $request );
121
122 if ( ! is_wp_error( $response ) ) {
123 $prepared[] = $this->prepare_response_for_collection( $response );
124 }
125 }
126 }
127
128 return new WP_REST_Response( $prepared );
129 }
130
131 /**
132 * Checks if a given request has access to get a widget.
133 *
134 * @since 5.6.0
135 *
136 * @param WP_REST_Request $request Full details about the request.
137 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
138 */
139 public function get_item_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
140 return $this->permissions_check();
141 }
142
143 /**
144 * Gets an individual widget.
145 *
146 * @since 5.6.0
147 *
148 * @param WP_REST_Request $request Full details about the request.
149 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
150 */
151 public function get_item( $request ) {
152 $widget_id = $request['id'];
153 $sidebar_id = gutenberg_find_widgets_sidebar( $widget_id );
154
155 if ( is_null( $sidebar_id ) ) {
156 return new WP_Error(
157 'rest_widget_not_found',
158 __( 'No widget was found with that id.', 'gutenberg' ),
159 array( 'status' => 404 )
160 );
161 }
162
163 return $this->prepare_item_for_response( compact( 'widget_id', 'sidebar_id' ), $request );
164 }
165
166 /**
167 * Checks if a given request has access to create widgets.
168 *
169 * @since 5.6.0
170 *
171 * @param WP_REST_Request $request Full details about the request.
172 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
173 */
174 public function create_item_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
175 return $this->permissions_check();
176 }
177
178 /**
179 * Creates a widget.
180 *
181 * @since 5.6.0
182 *
183 * @param WP_REST_Request $request Full details about the request.
184 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
185 */
186 public function create_item( $request ) {
187 $sidebar_id = $request['sidebar'];
188
189 $widget_id = $this->save_widget( $request );
190
191 if ( is_wp_error( $widget_id ) ) {
192 return $widget_id;
193 }
194
195 gutenberg_assign_widget_to_sidebar( $widget_id, $sidebar_id );
196
197 $request['context'] = 'edit';
198
199 $response = $this->prepare_item_for_response( compact( 'sidebar_id', 'widget_id' ), $request );
200
201 if ( is_wp_error( $response ) ) {
202 return $response;
203 }
204
205 $response->set_status( 201 );
206
207 return $response;
208 }
209
210 /**
211 * Checks if a given request has access to update widgets.
212 *
213 * @since 5.6.0
214 *
215 * @param WP_REST_Request $request Full details about the request.
216 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
217 */
218 public function update_item_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
219 return $this->permissions_check();
220 }
221
222 /**
223 * Updates an existing widget.
224 *
225 * @since 5.6.0
226 *
227 * @param WP_REST_Request $request Full details about the request.
228 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
229 */
230 public function update_item( $request ) {
231 $widget_id = $request['id'];
232 $sidebar_id = gutenberg_find_widgets_sidebar( $widget_id );
233
234 // Allow sidebar to be unset or missing when widget is not a WP_Widget.
235 $parsed_id = gutenberg_parse_widget_id( $widget_id );
236 $widget_object = gutenberg_get_widget_object( $parsed_id['id_base'] );
237 if ( is_null( $sidebar_id ) && $widget_object ) {
238 return new WP_Error(
239 'rest_widget_not_found',
240 __( 'No widget was found with that id.', 'gutenberg' ),
241 array( 'status' => 404 )
242 );
243 }
244
245 if (
246 $request->has_param( 'instance' ) ||
247 $request->has_param( 'form_data' )
248 ) {
249 $maybe_error = $this->save_widget( $request );
250 if ( is_wp_error( $maybe_error ) ) {
251 return $maybe_error;
252 }
253 }
254
255 if ( $request->has_param( 'sidebar' ) ) {
256 if ( $sidebar_id !== $request['sidebar'] ) {
257 $sidebar_id = $request['sidebar'];
258 gutenberg_assign_widget_to_sidebar( $widget_id, $sidebar_id );
259 }
260 }
261
262 $request['context'] = 'edit';
263
264 return $this->prepare_item_for_response( compact( 'widget_id', 'sidebar_id' ), $request );
265 }
266
267 /**
268 * Checks if a given request has access to delete widgets.
269 *
270 * @since 5.6.0
271 *
272 * @param WP_REST_Request $request Full details about the request.
273 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
274 */
275 public function delete_item_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
276 return $this->permissions_check();
277 }
278
279 /**
280 * Deletes a widget.
281 *
282 * @since 5.6.0
283 *
284 * @param WP_REST_Request $request Full details about the request.
285 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
286 */
287 public function delete_item( $request ) {
288 $widget_id = $request['id'];
289 $sidebar_id = gutenberg_find_widgets_sidebar( $widget_id );
290
291 if ( is_null( $sidebar_id ) ) {
292 return new WP_Error(
293 'rest_widget_not_found',
294 __( 'No widget was found with that id.', 'gutenberg' ),
295 array( 'status' => 404 )
296 );
297 }
298
299 $request['context'] = 'edit';
300
301 if ( $request['force'] ) {
302 $prepared = $this->prepare_item_for_response( compact( 'widget_id', 'sidebar_id' ), $request );
303 gutenberg_assign_widget_to_sidebar( $widget_id, '' );
304 $prepared->set_data(
305 array(
306 'deleted' => true,
307 'previous' => $prepared->get_data(),
308 )
309 );
310 } else {
311 gutenberg_assign_widget_to_sidebar( $widget_id, 'wp_inactive_widgets' );
312 $prepared = $this->prepare_item_for_response(
313 array(
314 'sidebar_id' => 'wp_inactive_widgets',
315 'widget_id' => $widget_id,
316 ),
317 $request
318 );
319 }
320
321 return $prepared;
322 }
323
324 /**
325 * Performs a permissions check for managing widgets.
326 *
327 * @since 5.6.0
328 *
329 * @return true|WP_Error
330 */
331 protected function permissions_check() {
332 if ( ! current_user_can( 'edit_theme_options' ) ) {
333 return new WP_Error(
334 'rest_cannot_manage_widgets',
335 __( 'Sorry, you are not allowed to manage widgets on this site.', 'gutenberg' ),
336 array(
337 'status' => rest_authorization_required_code(),
338 )
339 );
340 }
341
342 return true;
343 }
344
345 /**
346 * Saves the widget in the request object.
347 *
348 * @since 5.6.0
349 *
350 * @param WP_REST_Request $request Full details about the request.
351 *
352 * @return string The saved widget ID.
353 */
354 protected function save_widget( $request ) {
355 global $wp_registered_widget_updates;
356
357 require_once ABSPATH . 'wp-admin/includes/widgets.php'; // For next_widget_id_number().
358
359 if ( isset( $request['id'] ) ) {
360 // Saving an existing widget.
361 $id = $request['id'];
362 $parsed_id = gutenberg_parse_widget_id( $id );
363 $id_base = $parsed_id['id_base'];
364 $number = isset( $parsed_id['number'] ) ? $parsed_id['number'] : null;
365 $widget_object = gutenberg_get_widget_object( $id_base );
366 } elseif ( $request['id_base'] ) {
367 // Saving a new widget.
368 $id_base = $request['id_base'];
369 $widget_object = gutenberg_get_widget_object( $id_base );
370 $number = $widget_object ? next_widget_id_number( $id_base ) : null;
371 $id = $widget_object ? $id_base . '-' . $number : $id_base;
372 } else {
373 return new WP_Error(
374 'rest_invalid_widget',
375 __( 'Widget type (id_base) is required.', 'gutenberg' ),
376 array( 'status' => 400 )
377 );
378 }
379
380 if ( ! isset( $wp_registered_widget_updates[ $id_base ] ) ) {
381 return new WP_Error(
382 'rest_invalid_widget',
383 __( 'The provided widget type (id_base) cannot be updated.', 'gutenberg' ),
384 array( 'status' => 400 )
385 );
386 }
387
388 if ( isset( $request['instance'] ) ) {
389 if ( ! $widget_object ) {
390 return new WP_Error(
391 'rest_invalid_widget',
392 __( 'Cannot set instance on a widget that does not extend WP_Widget.', 'gutenberg' ),
393 array( 'status' => 400 )
394 );
395 }
396
397 if ( isset( $request['instance']['raw'] ) ) {
398 if ( empty( $widget_object->show_instance_in_rest ) ) {
399 return new WP_Error(
400 'rest_invalid_widget',
401 __( 'Widget type does not support raw instances.', 'gutenberg' ),
402 array( 'status' => 400 )
403 );
404 }
405 $instance = $request['instance']['raw'];
406 } elseif ( isset( $request['instance']['encoded'], $request['instance']['hash'] ) ) {
407 $serialized_instance = base64_decode( $request['instance']['encoded'] );
408 if ( ! hash_equals( wp_hash( $serialized_instance ), $request['instance']['hash'] ) ) {
409 return new WP_Error(
410 'rest_invalid_widget',
411 __( 'The provided instance is malformed.', 'gutenberg' ),
412 array( 'status' => 400 )
413 );
414 }
415 $instance = unserialize( $serialized_instance );
416 } else {
417 return new WP_Error(
418 'rest_invalid_widget',
419 __( 'The provided instance is invalid. Must contain raw OR encoded and hash.', 'gutenberg' ),
420 array( 'status' => 400 )
421 );
422 }
423
424 $form_data = array(
425 "widget-$id_base" => array(
426 $number => $instance,
427 ),
428 );
429 } elseif ( isset( $request['form_data'] ) ) {
430 $form_data = $request['form_data'];
431 } else {
432 $form_data = array();
433 }
434
435 $original_post = $_POST;
436 $original_request = $_REQUEST;
437
438 foreach ( $form_data as $key => $value ) {
439 $slashed_value = wp_slash( $value );
440 $_POST[ $key ] = $slashed_value;
441 $_REQUEST[ $key ] = $slashed_value;
442 }
443
444 $callback = $wp_registered_widget_updates[ $id_base ]['callback'];
445 $params = $wp_registered_widget_updates[ $id_base ]['params'];
446
447 if ( is_callable( $callback ) ) {
448 ob_start();
449 call_user_func_array( $callback, $params );
450 ob_end_clean();
451 }
452
453 $_POST = $original_post;
454 $_REQUEST = $original_request;
455
456 if ( $widget_object ) {
457 // Register any multi-widget that the update callback just created.
458 $widget_object->_set( $number );
459 $widget_object->_register_one( $number );
460
461 // WP_Widget sets updated = true after an update to prevent more
462 // than one widget from being saved per request. This isn't what we
463 // want in the REST API, though, as we support batch requests.
464 $widget_object->updated = false;
465 }
466
467 return $id;
468 }
469
470 /**
471 * Prepares the widget for the REST response.
472 *
473 * @since 5.6.0
474 *
475 * @global array $wp_registered_sidebars The registered sidebars.
476 * @global array $wp_registered_widgets The registered widgets.
477 * @global array $wp_registered_widget_controls The registered widget controls.
478 *
479 * @param array $item An array containing a widget_id and sidebar_id.
480 * @param WP_REST_Request $request Request object.
481 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
482 */
483 public function prepare_item_for_response( $item, $request ) {
484 global $wp_registered_widgets;
485
486 $widget_id = $item['widget_id'];
487 $sidebar_id = $item['sidebar_id'];
488
489 if ( ! isset( $wp_registered_widgets[ $widget_id ] ) ) {
490 return new WP_Error(
491 'rest_invalid_widget',
492 __( 'The requested widget is invalid.', 'gutenberg' ),
493 array( 'status' => 500 )
494 );
495 }
496
497 $widget = $wp_registered_widgets[ $widget_id ];
498 $parsed_id = gutenberg_parse_widget_id( $widget_id );
499 $fields = $this->get_fields_for_response( $request );
500
501 $prepared = array(
502 'id' => $widget_id,
503 'id_base' => $parsed_id['id_base'],
504 'sidebar' => $sidebar_id,
505 'rendered' => '',
506 'rendered_form' => null,
507 'instance' => null,
508 );
509
510 if (
511 rest_is_field_included( 'rendered', $fields ) &&
512 'wp_inactive_widgets' !== $sidebar_id
513 ) {
514 $prepared['rendered'] = trim( gutenberg_render_widget( $widget_id, $sidebar_id ) );
515 }
516
517 if ( rest_is_field_included( 'rendered_form', $fields ) ) {
518 $rendered_form = gutenberg_render_widget_control( $widget_id );
519 if ( ! is_null( $rendered_form ) ) {
520 $prepared['rendered_form'] = trim( $rendered_form );
521 }
522 }
523
524 if ( rest_is_field_included( 'instance', $fields ) ) {
525 $widget_object = gutenberg_get_widget_object( $parsed_id['id_base'] );
526 $instance = gutenberg_get_widget_instance( $widget_id );
527
528 if ( ! is_null( $instance ) ) {
529 $serialized_instance = serialize( $instance );
530 $prepared['instance']['encoded'] = base64_encode( $serialized_instance );
531 $prepared['instance']['hash'] = wp_hash( $serialized_instance );
532
533 if ( ! empty( $widget_object->show_instance_in_rest ) ) {
534 // Use new stdClass so that JSON result is {} and not [].
535 $prepared['instance']['raw'] = empty( $instance ) ? new stdClass : $instance;
536 }
537 }
538 }
539
540 $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
541 $prepared = $this->add_additional_fields_to_object( $prepared, $request );
542 $prepared = $this->filter_response_by_context( $prepared, $context );
543
544 $response = rest_ensure_response( $prepared );
545
546 $response->add_links( $this->prepare_links( $prepared ) );
547
548 /**
549 * Filters the REST API response for a widget.
550 *
551 * @since 5.6.0
552 *
553 * @param WP_REST_Response $response The response object.
554 * @param array $widget The registered widget data.
555 * @param WP_REST_Request $request Request used to generate the response.
556 */
557 return apply_filters( 'rest_prepare_widget', $response, $widget, $request );
558 }
559
560 /**
561 * Prepares links for the widget.
562 *
563 * @since 5.6.0
564 *
565 * @param array $prepared Widget.
566 * @return array Links for the given widget.
567 */
568 protected function prepare_links( $prepared ) {
569 $id_base = ! empty( $prepared['id_base'] ) ? $prepared['id_base'] : $prepared['id'];
570
571 return array(
572 'self' => array(
573 'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $prepared['id'] ) ),
574 ),
575 'collection' => array(
576 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
577 ),
578 'about' => array(
579 'href' => rest_url( sprintf( 'wp/v2/widget-types/%s', $id_base ) ),
580 'embeddable' => true,
581 ),
582 'https://api.w.org/sidebar' => array(
583 'href' => rest_url( sprintf( 'wp/v2/sidebars/%s/', $prepared['sidebar'] ) ),
584 ),
585 );
586 }
587
588 /**
589 * Gets the list of collection params.
590 *
591 * @since 5.6.0
592 *
593 * @return array[]
594 */
595 public function get_collection_params() {
596 return array(
597 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
598 'sidebar' => array(
599 'description' => __( 'The sidebar to return widgets for.', 'gutenberg' ),
600 'type' => 'string',
601 ),
602 );
603 }
604
605 /**
606 * Retrieves the widget's schema, conforming to JSON Schema.
607 *
608 * @since 5.6.0
609 *
610 * @return array Item schema data.
611 */
612 public function get_item_schema() {
613 if ( $this->schema ) {
614 return $this->add_additional_fields_schema( $this->schema );
615 }
616
617 $this->schema = array(
618 '$schema' => 'http://json-schema.org/draft-04/schema#',
619 'title' => 'widget',
620 'type' => 'object',
621 'properties' => array(
622 'id' => array(
623 'description' => __( 'Unique identifier for the widget.', 'gutenberg' ),
624 'type' => 'string',
625 'context' => array( 'view', 'edit', 'embed' ),
626 ),
627 'id_base' => array(
628 'description' => __( 'The type of the widget. Corresponds to ID in widget-types endpoint.', 'gutenberg' ),
629 'type' => 'string',
630 'context' => array( 'view', 'edit', 'embed' ),
631 ),
632 'sidebar' => array(
633 'description' => __( 'The sidebar the widget belongs to.', 'gutenberg' ),
634 'type' => 'string',
635 'default' => 'wp_inactive_widgets',
636 'required' => true,
637 'context' => array( 'view', 'edit', 'embed' ),
638 ),
639 'rendered' => array(
640 'description' => __( 'HTML representation of the widget.', 'gutenberg' ),
641 'type' => 'string',
642 'context' => array( 'view', 'edit', 'embed' ),
643 'readonly' => true,
644 ),
645 'rendered_form' => array(
646 'description' => __( 'HTML representation of the widget admin form.', 'gutenberg' ),
647 'type' => 'string',
648 'context' => array( 'edit' ),
649 'readonly' => true,
650 ),
651 'instance' => array(
652 'description' => __( 'Instance settings of the widget, if supported.', 'gutenberg' ),
653 'type' => 'object',
654 'context' => array( 'view', 'edit', 'embed' ),
655 'default' => null,
656 'properties' => array(
657 'encoded' => array(
658 'description' => __( 'Base64 encoded representation of the instance settings.', 'gutenberg' ),
659 'type' => 'string',
660 'context' => array( 'view', 'edit', 'embed' ),
661 ),
662 'hash' => array(
663 'description' => __( 'Cryptographic hash of the instance settings.', 'gutenberg' ),
664 'type' => 'string',
665 'context' => array( 'view', 'edit', 'embed' ),
666 ),
667 'raw' => array(
668 'description' => __( 'Unencoded instance settings, if supported.', 'gutenberg' ),
669 'type' => 'object',
670 'context' => array( 'view', 'edit', 'embed' ),
671 ),
672 ),
673 ),
674 'form_data' => array(
675 'description' => __( 'URL-encoded form data from the widget admin form. Used to update a widget that does not support instance. Write only.', 'gutenberg' ),
676 'type' => 'string',
677 'context' => array(),
678 'arg_options' => array(
679 'sanitize_callback' => function( $string ) {
680 $array = array();
681 wp_parse_str( $string, $array );
682 return $array;
683 },
684 ),
685 ),
686 ),
687 );
688
689 return $this->add_additional_fields_schema( $this->schema );
690 }
691 }
692