ru-se.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

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 }