class-wp-rest-widget-types-controller.php (15860B)
1 <?php 2 /** 3 * REST API: WP_REST_Widget_Types_Controller class 4 * 5 * @package WordPress 6 * @subpackage REST_API 7 * @since 5.8.0 8 */ 9 10 /** 11 * Core class to access widget types via the REST API. 12 * 13 * @since 5.8.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.8.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.8.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.' ), 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.' ), 78 'type' => 'string', 79 'required' => true, 80 ), 81 'instance' => array( 82 'description' => __( 'Current instance settings of the widget.' ), 83 'type' => 'object', 84 ), 85 'form_data' => array( 86 'description' => __( 'Serialized widget form data to encode into instance settings.' ), 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.8.0 108 * 109 * @param WP_REST_Request $request Full details about the request. 110 * @return true|WP_Error True if the request has read access, WP_Error object otherwise. 111 */ 112 public function get_items_permissions_check( $request ) { 113 return $this->check_read_permission(); 114 } 115 116 /** 117 * Retrieves the list of all widget types. 118 * 119 * @since 5.8.0 120 * 121 * @param WP_REST_Request $request Full details about the request. 122 * @return WP_REST_Response|WP_Error 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.8.0 138 * 139 * @param WP_REST_Request $request Full details about the request. 140 * @return true|WP_Error 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.8.0 160 * 161 * @return true|WP_Error 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.' ), 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.8.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.' ), array( 'status' => 404 ) ); 193 } 194 195 /** 196 * Normalize array of widgets. 197 * 198 * @since 5.8.0 199 * 200 * @global WP_Widget_Factory $wp_widget_factory 201 * @global array $wp_registered_widgets The list of registered widgets. 202 * 203 * @return array Array of widgets. 204 */ 205 protected function get_widgets() { 206 global $wp_widget_factory, $wp_registered_widgets; 207 208 $widgets = array(); 209 210 foreach ( $wp_registered_widgets as $widget ) { 211 $parsed_id = wp_parse_widget_id( $widget['id'] ); 212 $widget_object = $wp_widget_factory->get_widget_object( $parsed_id['id_base'] ); 213 214 $widget['id'] = $parsed_id['id_base']; 215 $widget['is_multi'] = (bool) $widget_object; 216 217 if ( isset( $widget['name'] ) ) { 218 $widget['name'] = html_entity_decode( $widget['name'], ENT_QUOTES, get_bloginfo( 'charset' ) ); 219 } 220 221 if ( isset( $widget['description'] ) ) { 222 $widget['description'] = html_entity_decode( $widget['description'], ENT_QUOTES, get_bloginfo( 'charset' ) ); 223 } 224 225 unset( $widget['callback'] ); 226 227 $classname = ''; 228 foreach ( (array) $widget['classname'] as $cn ) { 229 if ( is_string( $cn ) ) { 230 $classname .= '_' . $cn; 231 } elseif ( is_object( $cn ) ) { 232 $classname .= '_' . get_class( $cn ); 233 } 234 } 235 $widget['classname'] = ltrim( $classname, '_' ); 236 237 $widgets[ $widget['id'] ] = $widget; 238 } 239 240 return $widgets; 241 } 242 243 /** 244 * Retrieves a single widget type from the collection. 245 * 246 * @since 5.8.0 247 * 248 * @param WP_REST_Request $request Full details about the request. 249 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 250 */ 251 public function get_item( $request ) { 252 $widget_id = $request['id']; 253 $widget_type = $this->get_widget( $widget_id ); 254 if ( is_wp_error( $widget_type ) ) { 255 return $widget_type; 256 } 257 $data = $this->prepare_item_for_response( $widget_type, $request ); 258 259 return rest_ensure_response( $data ); 260 } 261 262 /** 263 * Prepares a widget type object for serialization. 264 * 265 * @since 5.8.0 266 * 267 * @param array $widget_type Widget type data. 268 * @param WP_REST_Request $request Full details about the request. 269 * @return WP_REST_Response Widget type data. 270 */ 271 public function prepare_item_for_response( $widget_type, $request ) { 272 $fields = $this->get_fields_for_response( $request ); 273 $data = array( 274 'id' => $widget_type['id'], 275 ); 276 277 $schema = $this->get_item_schema(); 278 $extra_fields = array( 279 'name', 280 'description', 281 'is_multi', 282 'classname', 283 'widget_class', 284 'option_name', 285 'customize_selective_refresh', 286 ); 287 288 foreach ( $extra_fields as $extra_field ) { 289 if ( ! rest_is_field_included( $extra_field, $fields ) ) { 290 continue; 291 } 292 293 if ( isset( $widget_type[ $extra_field ] ) ) { 294 $field = $widget_type[ $extra_field ]; 295 } elseif ( array_key_exists( 'default', $schema['properties'][ $extra_field ] ) ) { 296 $field = $schema['properties'][ $extra_field ]['default']; 297 } else { 298 $field = ''; 299 } 300 301 $data[ $extra_field ] = rest_sanitize_value_from_schema( $field, $schema['properties'][ $extra_field ] ); 302 } 303 304 $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; 305 $data = $this->add_additional_fields_to_object( $data, $request ); 306 $data = $this->filter_response_by_context( $data, $context ); 307 308 $response = rest_ensure_response( $data ); 309 310 $response->add_links( $this->prepare_links( $widget_type ) ); 311 312 /** 313 * Filters the REST API response for a widget type. 314 * 315 * @since 5.8.0 316 * 317 * @param WP_REST_Response $response The response object. 318 * @param array $widget_type The array of widget data. 319 * @param WP_REST_Request $request The request object. 320 */ 321 return apply_filters( 'rest_prepare_widget_type', $response, $widget_type, $request ); 322 } 323 324 /** 325 * Prepares links for the widget type. 326 * 327 * @since 5.8.0 328 * 329 * @param array $widget_type Widget type data. 330 * @return array Links for the given widget type. 331 */ 332 protected function prepare_links( $widget_type ) { 333 return array( 334 'collection' => array( 335 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), 336 ), 337 'self' => array( 338 'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $widget_type['id'] ) ), 339 ), 340 ); 341 } 342 343 /** 344 * Retrieves the widget type's schema, conforming to JSON Schema. 345 * 346 * @since 5.8.0 347 * 348 * @return array Item schema data. 349 */ 350 public function get_item_schema() { 351 if ( $this->schema ) { 352 return $this->add_additional_fields_schema( $this->schema ); 353 } 354 355 $schema = array( 356 '$schema' => 'http://json-schema.org/draft-04/schema#', 357 'title' => 'widget-type', 358 'type' => 'object', 359 'properties' => array( 360 'id' => array( 361 'description' => __( 'Unique slug identifying the widget type.' ), 362 'type' => 'string', 363 'context' => array( 'embed', 'view', 'edit' ), 364 'readonly' => true, 365 ), 366 'name' => array( 367 'description' => __( 'Human-readable name identifying the widget type.' ), 368 'type' => 'string', 369 'default' => '', 370 'context' => array( 'embed', 'view', 'edit' ), 371 'readonly' => true, 372 ), 373 'description' => array( 374 'description' => __( 'Description of the widget.' ), 375 'type' => 'string', 376 'default' => '', 377 'context' => array( 'view', 'edit', 'embed' ), 378 ), 379 'is_multi' => array( 380 'description' => __( 'Whether the widget supports multiple instances' ), 381 'type' => 'boolean', 382 'context' => array( 'view', 'edit', 'embed' ), 383 'readonly' => true, 384 ), 385 'classname' => array( 386 'description' => __( 'Class name' ), 387 'type' => 'string', 388 'default' => '', 389 'context' => array( 'embed', 'view', 'edit' ), 390 'readonly' => true, 391 ), 392 ), 393 ); 394 395 $this->schema = $schema; 396 397 return $this->add_additional_fields_schema( $this->schema ); 398 } 399 400 /** 401 * An RPC-style endpoint which can be used by clients to turn user input in 402 * a widget admin form into an encoded instance object. 403 * 404 * Accepts: 405 * 406 * - id: A widget type ID. 407 * - instance: A widget's encoded instance object. Optional. 408 * - form_data: Form data from submitting a widget's admin form. Optional. 409 * 410 * Returns: 411 * - instance: The encoded instance object after updating the widget with 412 * the given form data. 413 * - form: The widget's admin form after updating the widget with the 414 * given form data. 415 * 416 * @since 5.8.0 417 * 418 * @global WP_Widget_Factory $wp_widget_factory 419 * 420 * @param WP_REST_Request $request Full details about the request. 421 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 422 */ 423 public function encode_form_data( $request ) { 424 global $wp_widget_factory; 425 426 $id = $request['id']; 427 $widget_object = $wp_widget_factory->get_widget_object( $id ); 428 429 if ( ! $widget_object ) { 430 return new WP_Error( 431 'rest_invalid_widget', 432 __( 'Cannot preview a widget that does not extend WP_Widget.' ), 433 array( 'status' => 400 ) 434 ); 435 } 436 437 // Set the widget's number so that the id attributes in the HTML that we 438 // return are predictable. 439 if ( isset( $request['number'] ) && is_numeric( $request['number'] ) ) { 440 $widget_object->_set( (int) $request['number'] ); 441 } else { 442 $widget_object->_set( -1 ); 443 } 444 445 if ( isset( $request['instance']['encoded'], $request['instance']['hash'] ) ) { 446 $serialized_instance = base64_decode( $request['instance']['encoded'] ); 447 if ( ! hash_equals( wp_hash( $serialized_instance ), $request['instance']['hash'] ) ) { 448 return new WP_Error( 449 'rest_invalid_widget', 450 __( 'The provided instance is malformed.' ), 451 array( 'status' => 400 ) 452 ); 453 } 454 $instance = unserialize( $serialized_instance ); 455 } else { 456 $instance = array(); 457 } 458 459 if ( 460 isset( $request['form_data'][ "widget-$id" ] ) && 461 is_array( $request['form_data'][ "widget-$id" ] ) 462 ) { 463 $new_instance = array_values( $request['form_data'][ "widget-$id" ] )[0]; 464 $old_instance = $instance; 465 466 $instance = $widget_object->update( $new_instance, $old_instance ); 467 468 /** This filter is documented in wp-includes/class-wp-widget.php */ 469 $instance = apply_filters( 470 'widget_update_callback', 471 $instance, 472 $new_instance, 473 $old_instance, 474 $widget_object 475 ); 476 } 477 478 $serialized_instance = serialize( $instance ); 479 $widget_key = $wp_widget_factory->get_widget_key( $id ); 480 481 $response = array( 482 'form' => trim( 483 $this->get_widget_form( 484 $widget_object, 485 $instance 486 ) 487 ), 488 'preview' => trim( 489 $this->get_widget_preview( 490 $widget_key, 491 $instance 492 ) 493 ), 494 'instance' => array( 495 'encoded' => base64_encode( $serialized_instance ), 496 'hash' => wp_hash( $serialized_instance ), 497 ), 498 ); 499 500 if ( ! empty( $widget_object->widget_options['show_instance_in_rest'] ) ) { 501 // Use new stdClass so that JSON result is {} and not []. 502 $response['instance']['raw'] = empty( $instance ) ? new stdClass : $instance; 503 } 504 505 return rest_ensure_response( $response ); 506 } 507 508 /** 509 * Returns the output of WP_Widget::widget() when called with the provided 510 * instance. Used by encode_form_data() to preview a widget. 511 512 * @since 5.8.0 513 * 514 * @param string $widget The widget's PHP class name (see class-wp-widget.php). 515 * @param array $instance Widget instance settings. 516 * @return string 517 */ 518 private function get_widget_preview( $widget, $instance ) { 519 ob_start(); 520 the_widget( $widget, $instance ); 521 return ob_get_clean(); 522 } 523 524 /** 525 * Returns the output of WP_Widget::form() when called with the provided 526 * instance. Used by encode_form_data() to preview a widget's form. 527 * 528 * @since 5.8.0 529 * 530 * @param WP_Widget $widget_object Widget object to call widget() on. 531 * @param array $instance Widget instance settings. 532 * @return string 533 */ 534 private function get_widget_form( $widget_object, $instance ) { 535 ob_start(); 536 537 /** This filter is documented in wp-includes/class-wp-widget.php */ 538 $instance = apply_filters( 539 'widget_form_callback', 540 $instance, 541 $widget_object 542 ); 543 544 if ( false !== $instance ) { 545 $return = $widget_object->form( $instance ); 546 547 /** This filter is documented in wp-includes/class-wp-widget.php */ 548 do_action_ref_array( 549 'in_widget_form', 550 array( &$widget_object, &$return, $instance ) 551 ); 552 } 553 554 return ob_get_clean(); 555 } 556 557 /** 558 * Retrieves the query params for collections. 559 * 560 * @since 5.8.0 561 * 562 * @return array Collection parameters. 563 */ 564 public function get_collection_params() { 565 return array( 566 'context' => $this->get_context_param( array( 'default' => 'view' ) ), 567 ); 568 } 569 }