Нема описа

class-wp-rest-pattern-directory-controller.php 10KB

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