wp/wp-includes/rest-api/endpoints/class-wp-rest-pattern-directory-controller.php
changeset 18 be944660c56a
child 19 3d72ae0968f4
equal deleted inserted replaced
17:34716fd837a4 18:be944660c56a
       
     1 <?php
       
     2 /**
       
     3  * Block Pattern Directory REST API: WP_REST_Pattern_Directory_Controller class
       
     4  *
       
     5  * @package WordPress
       
     6  * @subpackage REST_API
       
     7  * @since 5.8.0
       
     8  */
       
     9 
       
    10 /**
       
    11  * Controller which provides REST endpoint for block patterns.
       
    12  *
       
    13  * This simply proxies the endpoint at http://api.wordpress.org/patterns/1.0/. That isn't necessary for
       
    14  * functionality, but is desired for privacy. It prevents api.wordpress.org from knowing the user's IP address.
       
    15  *
       
    16  * @since 5.8.0
       
    17  *
       
    18  * @see WP_REST_Controller
       
    19  */
       
    20 class WP_REST_Pattern_Directory_Controller extends WP_REST_Controller {
       
    21 
       
    22 	/**
       
    23 	 * Constructs the controller.
       
    24 	 *
       
    25 	 * @since 5.8.0
       
    26 	 */
       
    27 	public function __construct() {
       
    28 		$this->namespace     = 'wp/v2';
       
    29 			$this->rest_base = 'pattern-directory';
       
    30 	}
       
    31 
       
    32 	/**
       
    33 	 * Registers the necessary REST API routes.
       
    34 	 *
       
    35 	 * @since 5.8.0
       
    36 	 */
       
    37 	public function register_routes() {
       
    38 		register_rest_route(
       
    39 			$this->namespace,
       
    40 			'/' . $this->rest_base . '/patterns',
       
    41 			array(
       
    42 				array(
       
    43 					'methods'             => WP_REST_Server::READABLE,
       
    44 					'callback'            => array( $this, 'get_items' ),
       
    45 					'permission_callback' => array( $this, 'get_items_permissions_check' ),
       
    46 					'args'                => $this->get_collection_params(),
       
    47 				),
       
    48 				'schema' => array( $this, 'get_public_item_schema' ),
       
    49 			)
       
    50 		);
       
    51 	}
       
    52 
       
    53 	/**
       
    54 	 * Checks whether a given request has permission to view the local pattern directory.
       
    55 	 *
       
    56 	 * @since 5.8.0
       
    57 	 *
       
    58 	 * @param WP_REST_Request $request Full details about the request.
       
    59 	 * @return true|WP_Error True if the request has permission, WP_Error object otherwise.
       
    60 	 */
       
    61 	public function get_items_permissions_check( $request ) {
       
    62 		if ( current_user_can( 'edit_posts' ) ) {
       
    63 			return true;
       
    64 		}
       
    65 
       
    66 		foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
       
    67 			if ( current_user_can( $post_type->cap->edit_posts ) ) {
       
    68 				return true;
       
    69 			}
       
    70 		}
       
    71 
       
    72 		return new WP_Error(
       
    73 			'rest_pattern_directory_cannot_view',
       
    74 			__( 'Sorry, you are not allowed to browse the local block pattern directory.' ),
       
    75 			array( 'status' => rest_authorization_required_code() )
       
    76 		);
       
    77 	}
       
    78 
       
    79 	/**
       
    80 	 * Search and retrieve block patterns metadata
       
    81 	 *
       
    82 	 * @since 5.8.0
       
    83 	 *
       
    84 	 * @param WP_REST_Request $request Full details about the request.
       
    85 	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
       
    86 	 */
       
    87 	public function get_items( $request ) {
       
    88 		/*
       
    89 		 * Include an unmodified `$wp_version`, so the API can craft a response that's tailored to
       
    90 		 * it. Some plugins modify the version in a misguided attempt to improve security by
       
    91 		 * obscuring the version, which can cause invalid requests.
       
    92 		 */
       
    93 		require ABSPATH . WPINC . '/version.php';
       
    94 
       
    95 		$query_args = array(
       
    96 			'locale'     => get_user_locale(),
       
    97 			'wp-version' => $wp_version,
       
    98 		);
       
    99 
       
   100 		$category_id = $request['category'];
       
   101 		$keyword_id  = $request['keyword'];
       
   102 		$search_term = $request['search'];
       
   103 
       
   104 		if ( $category_id ) {
       
   105 			$query_args['pattern-categories'] = $category_id;
       
   106 		}
       
   107 
       
   108 		if ( $keyword_id ) {
       
   109 			$query_args['pattern-keywords'] = $keyword_id;
       
   110 		}
       
   111 
       
   112 		if ( $search_term ) {
       
   113 			$query_args['search'] = $search_term;
       
   114 		}
       
   115 
       
   116 		/*
       
   117 		 * Include a hash of the query args, so that different requests are stored in
       
   118 		 * separate caches.
       
   119 		 *
       
   120 		 * MD5 is chosen for its speed, low-collision rate, universal availability, and to stay
       
   121 		 * under the character limit for `_site_transient_timeout_{...}` keys.
       
   122 		 *
       
   123 		 * @link https://stackoverflow.com/questions/3665247/fastest-hash-for-non-cryptographic-uses
       
   124 		 */
       
   125 		$transient_key = 'wp_remote_block_patterns_' . md5( implode( '-', $query_args ) );
       
   126 
       
   127 		/*
       
   128 		 * Use network-wide transient to improve performance. The locale is the only site
       
   129 		 * configuration that affects the response, and it's included in the transient key.
       
   130 		 */
       
   131 		$raw_patterns = get_site_transient( $transient_key );
       
   132 
       
   133 		if ( ! $raw_patterns ) {
       
   134 			$api_url = add_query_arg(
       
   135 				array_map( 'rawurlencode', $query_args ),
       
   136 				'http://api.wordpress.org/patterns/1.0/'
       
   137 			);
       
   138 
       
   139 			if ( wp_http_supports( array( 'ssl' ) ) ) {
       
   140 				$api_url = set_url_scheme( $api_url, 'https' );
       
   141 			}
       
   142 
       
   143 			/*
       
   144 			 * Default to a short TTL, to mitigate cache stampedes on high-traffic sites.
       
   145 			 * This assumes that most errors will be short-lived, e.g., packet loss that causes the
       
   146 			 * first request to fail, but a follow-up one will succeed. The value should be high
       
   147 			 * enough to avoid stampedes, but low enough to not interfere with users manually
       
   148 			 * re-trying a failed request.
       
   149 			 */
       
   150 			$cache_ttl      = 5;
       
   151 			$wporg_response = wp_remote_get( $api_url );
       
   152 			$raw_patterns   = json_decode( wp_remote_retrieve_body( $wporg_response ) );
       
   153 
       
   154 			if ( is_wp_error( $wporg_response ) ) {
       
   155 				$raw_patterns = $wporg_response;
       
   156 
       
   157 			} elseif ( ! is_array( $raw_patterns ) ) {
       
   158 				// HTTP request succeeded, but response data is invalid.
       
   159 				$raw_patterns = new WP_Error(
       
   160 					'pattern_api_failed',
       
   161 					sprintf(
       
   162 					/* translators: %s: Support forums URL. */
       
   163 						__( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.' ),
       
   164 						__( 'https://wordpress.org/support/forums/' )
       
   165 					),
       
   166 					array(
       
   167 						'response' => wp_remote_retrieve_body( $wporg_response ),
       
   168 					)
       
   169 				);
       
   170 
       
   171 			} else {
       
   172 				// Response has valid data.
       
   173 				$cache_ttl = HOUR_IN_SECONDS;
       
   174 			}
       
   175 
       
   176 			set_site_transient( $transient_key, $raw_patterns, $cache_ttl );
       
   177 		}
       
   178 
       
   179 		if ( is_wp_error( $raw_patterns ) ) {
       
   180 			$raw_patterns->add_data( array( 'status' => 500 ) );
       
   181 
       
   182 			return $raw_patterns;
       
   183 		}
       
   184 
       
   185 		$response = array();
       
   186 
       
   187 		if ( $raw_patterns ) {
       
   188 			foreach ( $raw_patterns as $pattern ) {
       
   189 				$response[] = $this->prepare_response_for_collection(
       
   190 					$this->prepare_item_for_response( $pattern, $request )
       
   191 				);
       
   192 			}
       
   193 		}
       
   194 
       
   195 		return new WP_REST_Response( $response );
       
   196 	}
       
   197 
       
   198 	/**
       
   199 	 * Prepare a raw pattern before it's output in an API response.
       
   200 	 *
       
   201 	 * @since 5.8.0
       
   202 	 *
       
   203 	 * @param object          $raw_pattern A pattern from api.wordpress.org, before any changes.
       
   204 	 * @param WP_REST_Request $request     Request object.
       
   205 	 * @return WP_REST_Response
       
   206 	 */
       
   207 	public function prepare_item_for_response( $raw_pattern, $request ) {
       
   208 		$prepared_pattern = array(
       
   209 			'id'             => absint( $raw_pattern->id ),
       
   210 			'title'          => sanitize_text_field( $raw_pattern->title->rendered ),
       
   211 			'content'        => wp_kses_post( $raw_pattern->pattern_content ),
       
   212 			'categories'     => array_map( 'sanitize_title', $raw_pattern->category_slugs ),
       
   213 			'keywords'       => array_map( 'sanitize_title', $raw_pattern->keyword_slugs ),
       
   214 			'description'    => sanitize_text_field( $raw_pattern->meta->wpop_description ),
       
   215 			'viewport_width' => absint( $raw_pattern->meta->wpop_viewport_width ),
       
   216 		);
       
   217 
       
   218 		$prepared_pattern = $this->add_additional_fields_to_object( $prepared_pattern, $request );
       
   219 
       
   220 		$response = new WP_REST_Response( $prepared_pattern );
       
   221 
       
   222 		/**
       
   223 		 * Filters the REST API response for a pattern.
       
   224 		 *
       
   225 		 * @since 5.8.0
       
   226 		 *
       
   227 		 * @param WP_REST_Response $response    The response object.
       
   228 		 * @param object           $raw_pattern The unprepared pattern.
       
   229 		 * @param WP_REST_Request  $request     The request object.
       
   230 		 */
       
   231 		return apply_filters( 'rest_prepare_block_pattern', $response, $raw_pattern, $request );
       
   232 	}
       
   233 
       
   234 	/**
       
   235 	 * Retrieves the pattern's schema, conforming to JSON Schema.
       
   236 	 *
       
   237 	 * @since 5.8.0
       
   238 	 *
       
   239 	 * @return array Item schema data.
       
   240 	 */
       
   241 	public function get_item_schema() {
       
   242 		if ( $this->schema ) {
       
   243 			return $this->add_additional_fields_schema( $this->schema );
       
   244 		}
       
   245 
       
   246 		$this->schema = array(
       
   247 			'$schema'    => 'http://json-schema.org/draft-04/schema#',
       
   248 			'title'      => 'pattern-directory-item',
       
   249 			'type'       => 'object',
       
   250 			'properties' => array(
       
   251 				'id'             => array(
       
   252 					'description' => __( 'The pattern ID.' ),
       
   253 					'type'        => 'integer',
       
   254 					'minimum'     => 1,
       
   255 					'context'     => array( 'view', 'embed' ),
       
   256 				),
       
   257 
       
   258 				'title'          => array(
       
   259 					'description' => __( 'The pattern title, in human readable format.' ),
       
   260 					'type'        => 'string',
       
   261 					'minLength'   => 1,
       
   262 					'context'     => array( 'view', 'embed' ),
       
   263 				),
       
   264 
       
   265 				'content'        => array(
       
   266 					'description' => __( 'The pattern content.' ),
       
   267 					'type'        => 'string',
       
   268 					'minLength'   => 1,
       
   269 					'context'     => array( 'view', 'embed' ),
       
   270 				),
       
   271 
       
   272 				'categories'     => array(
       
   273 					'description' => __( "The pattern's category slugs." ),
       
   274 					'type'        => 'array',
       
   275 					'uniqueItems' => true,
       
   276 					'items'       => array( 'type' => 'string' ),
       
   277 					'context'     => array( 'view', 'embed' ),
       
   278 				),
       
   279 
       
   280 				'keywords'       => array(
       
   281 					'description' => __( "The pattern's keyword slugs." ),
       
   282 					'type'        => 'array',
       
   283 					'uniqueItems' => true,
       
   284 					'items'       => array( 'type' => 'string' ),
       
   285 					'context'     => array( 'view', 'embed' ),
       
   286 				),
       
   287 
       
   288 				'description'    => array(
       
   289 					'description' => __( 'A description of the pattern.' ),
       
   290 					'type'        => 'string',
       
   291 					'minLength'   => 1,
       
   292 					'context'     => array( 'view', 'embed' ),
       
   293 				),
       
   294 
       
   295 				'viewport_width' => array(
       
   296 					'description' => __( 'The preferred width of the viewport when previewing a pattern, in pixels.' ),
       
   297 					'type'        => 'integer',
       
   298 					'context'     => array( 'view', 'embed' ),
       
   299 				),
       
   300 			),
       
   301 		);
       
   302 
       
   303 		return $this->add_additional_fields_schema( $this->schema );
       
   304 	}
       
   305 
       
   306 	/**
       
   307 	 * Retrieves the search params for the patterns collection.
       
   308 	 *
       
   309 	 * @since 5.8.0
       
   310 	 *
       
   311 	 * @return array Collection parameters.
       
   312 	 */
       
   313 	public function get_collection_params() {
       
   314 		$query_params = parent::get_collection_params();
       
   315 
       
   316 		// Pagination is not supported.
       
   317 		unset( $query_params['page'] );
       
   318 		unset( $query_params['per_page'] );
       
   319 
       
   320 		$query_params['search']['minLength'] = 1;
       
   321 		$query_params['context']['default']  = 'view';
       
   322 
       
   323 		$query_params['category'] = array(
       
   324 			'description' => __( 'Limit results to those matching a category ID.' ),
       
   325 			'type'        => 'integer',
       
   326 			'minimum'     => 1,
       
   327 		);
       
   328 
       
   329 		$query_params['keyword'] = array(
       
   330 			'description' => __( 'Limit results to those matching a keyword ID.' ),
       
   331 			'type'        => 'integer',
       
   332 			'minimum'     => 1,
       
   333 		);
       
   334 
       
   335 		/**
       
   336 		 * Filter collection parameters for the pattern directory controller.
       
   337 		 *
       
   338 		 * @since 5.8.0
       
   339 		 *
       
   340 		 * @param array $query_params JSON Schema-formatted collection parameters.
       
   341 		 */
       
   342 		return apply_filters( 'rest_pattern_directory_collection_params', $query_params );
       
   343 	}
       
   344 }