class-acf-rest-request.php 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. <?php
  2. // Exit if accessed directly.
  3. if ( ! defined( 'ABSPATH' ) ) {
  4. exit;
  5. }
  6. // If class is already defined, return.
  7. if ( class_exists( 'ACF_Rest_Request' ) ) {
  8. return;
  9. }
  10. /**
  11. * Class ACF_Rest_Request
  12. *
  13. * @property-read string $object_sub_type
  14. * @property-read string $object_type
  15. * @property-read string $http_method
  16. */
  17. class ACF_Rest_Request {
  18. /**
  19. * Define which private/protected class properties are allowed read access. Access to these is controlled in
  20. * \ACF_Rest_Request::__get();
  21. *
  22. * @var string[]
  23. */
  24. private $readonly_props = array( 'object_type', 'object_sub_type', 'child_object_type', 'http_method' );
  25. /** @var string The HTTP request method for the current request. i.e; GET, POST, PATCH, PUT, DELETE, OPTIONS, HEAD */
  26. private $http_method;
  27. /** @var string The current route being requested. */
  28. private $current_route;
  29. /** @var array Route URL patterns we support. */
  30. private $supported_routes = array();
  31. /** @var array Parameters matched from the URL. e.g; object IDs. */
  32. private $url_params = array();
  33. /** @var string The underlying object type. e.g; post, term, user, etc. */
  34. private $object_type;
  35. /** @var string The requested object type. */
  36. private $object_sub_type;
  37. /** @var string The object type for a child object. e.g. post-revision, autosaves, etc. */
  38. private $child_object_type;
  39. /**
  40. * Determine all required information from the current request.
  41. */
  42. public function parse_request( $request ) {
  43. $this->set_http_method();
  44. $this->set_current_route( $request );
  45. $this->build_supported_routes();
  46. $this->set_url_params();
  47. $this->set_object_types();
  48. }
  49. /**
  50. * Magic getter for accessing read-only properties. Should we ever need to enforce a getter method, we can do so here.
  51. *
  52. * @param string $name The desired property name.
  53. * @return string|null
  54. */
  55. public function __get( $name ) {
  56. if ( in_array( $name, $this->readonly_props ) ) {
  57. return $this->$name;
  58. }
  59. return null;
  60. }
  61. /**
  62. * Get a URL parameter if found on the request URL.
  63. *
  64. * @param $param
  65. * @return mixed|null
  66. */
  67. public function get_url_param( $param ) {
  68. return isset( $this->url_params[ $param ] ) ? $this->url_params[ $param ] : null;
  69. }
  70. /**
  71. * Determine the HTTP method of the current request.
  72. */
  73. private function set_http_method() {
  74. $this->http_method = 'GET';
  75. if ( ! empty( $_SERVER['REQUEST_METHOD'] ) ) {
  76. $this->http_method = strtoupper( sanitize_text_field( $_SERVER['REQUEST_METHOD'] ) );
  77. }
  78. // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Verified elsewhere.
  79. // HTTP method override for clients that can't use PUT/PATCH/DELETE. This is identical to WordPress'
  80. // handling in \WP_REST_Server::serve_request(). This block of code should always be identical to that
  81. // in core.
  82. if ( isset( $_GET['_method'] ) ) {
  83. $this->http_method = strtoupper( sanitize_text_field( $_GET['_method'] ) );
  84. } elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
  85. $this->http_method = strtoupper( sanitize_text_field( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) );
  86. }
  87. // phpcs:enable WordPress.Security.NonceVerification.Recommended
  88. }
  89. /**
  90. * Get the current REST route as determined by WordPress.
  91. */
  92. private function set_current_route( $request ) {
  93. if ( $request ) {
  94. $this->current_route = $request->get_route();
  95. } else {
  96. $this->current_route = empty( $GLOBALS['wp']->query_vars['rest_route'] ) ? null : $GLOBALS['wp']->query_vars['rest_route'];
  97. }
  98. }
  99. /**
  100. * Build an array of route match patterns that we handle. These are the same as WordPress' core patterns except
  101. * we are also matching the object type here as well.
  102. */
  103. private function build_supported_routes() {
  104. // Add post type routes for all post types configured to show in REST.
  105. /** @var WP_Post_Type $post_type */
  106. foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
  107. $rest_base = acf_get_object_type_rest_base( $post_type );
  108. $this->supported_routes[] = "/wp/v2/(?P<rest_base>{$rest_base})";
  109. $this->supported_routes[] = "/wp/v2/(?P<rest_base>{$rest_base})/(?P<id>[\d]+)";
  110. if ( post_type_supports( $post_type->name, 'revisions' ) ) {
  111. $this->supported_routes[] = "/wp/v2/(?P<rest_base>{$rest_base})/(?P<id>[\d]+)/(?P<child_rest_base>revisions)";
  112. $this->supported_routes[] = "/wp/v2/(?P<rest_base>{$rest_base})/(?P<id>[\d]+)/(?P<child_rest_base>revisions)/(?P<child_id>[\d]+)";
  113. }
  114. if ( 'attachment' !== $post_type->name ) {
  115. $this->supported_routes[] = "/wp/v2/(?P<rest_base>{$rest_base})/(?P<id>[\d]+)/(?P<child_rest_base>autosaves)";
  116. $this->supported_routes[] = "/wp/v2/(?P<rest_base>{$rest_base})/(?P<id>[\d]+)/(?P<child_rest_base>autosaves)/(?P<child_id>[\d]+)";
  117. }
  118. }
  119. // Add taxonomy routes all taxonomies configured to show in REST.
  120. /** @var WP_Taxonomy $taxonomy */
  121. foreach ( get_taxonomies( array( 'show_in_rest' => true ), 'object' ) as $taxonomy ) {
  122. $rest_base = acf_get_object_type_rest_base( $taxonomy );
  123. $this->supported_routes[] = "/wp/v2/(?P<rest_base>{$rest_base})";
  124. $this->supported_routes[] = "/wp/v2/(?P<rest_base>{$rest_base})/(?P<id>[\d]+)";
  125. }
  126. // Add user routes.
  127. $this->supported_routes[] = '/wp/v2/(?P<rest_base>users)';
  128. $this->supported_routes[] = '/wp/v2/(?P<rest_base>users)/(?P<id>[\d]+)';
  129. $this->supported_routes[] = '/wp/v2/(?P<rest_base>users)/me';
  130. // Add comment routes.
  131. $this->supported_routes[] = '/wp/v2/(?P<rest_base>comments)';
  132. $this->supported_routes[] = '/wp/v2/(?P<rest_base>comments)/(?P<id>[\d]+)';
  133. }
  134. /**
  135. * Loop through supported routes to find matching pattern. Use matching pattern to determine any URL parameters.
  136. */
  137. private function set_url_params() {
  138. if ( ! $this->supported_routes || ! is_string( $this->current_route ) ) {
  139. return;
  140. }
  141. // Determine query args passed within the URL.
  142. foreach ( $this->supported_routes as $route ) {
  143. $match = preg_match( '@^' . $route . '$@i', $this->current_route, $matches );
  144. if ( ! $match ) {
  145. continue;
  146. }
  147. foreach ( $matches as $param => $value ) {
  148. if ( ! is_int( $param ) ) {
  149. $this->url_params[ $param ] = $value;
  150. }
  151. }
  152. }
  153. }
  154. /**
  155. * Determine the object type and sub type from the requested route. We need to know both the underlying WordPress
  156. * object type as well as post type or taxonomy in order to provide the right context when getting/updating fields.
  157. */
  158. private function set_object_types() {
  159. $base = $this->get_url_param( 'rest_base' );
  160. $child_base = $this->get_url_param( 'child_rest_base' );
  161. // We need a matched rest base to proceed here. If we haven't matched one while parsing the request, bail.
  162. if ( is_null( $base ) ) {
  163. return;
  164. }
  165. // Determine the matching object type from the rest base. Start with users as that is simple. From there,
  166. // check post types then check taxonomies if a matching post type cannot be found.
  167. if ( $base === 'users' ) {
  168. $this->object_type = $this->object_sub_type = 'user';
  169. } elseif ( $base === 'comments' ) {
  170. $this->object_type = $this->object_sub_type = 'comment';
  171. } elseif ( $post_type = $this->get_post_type_by_rest_base( $base ) ) {
  172. $this->object_type = 'post';
  173. $this->object_sub_type = $post_type->name;
  174. // Autosaves and revisions are mostly handled the same by WP, and share the same schema.
  175. if ( in_array( $this->get_url_param( 'child_rest_base' ), array( 'revisions', 'autosaves' ) ) ) {
  176. $this->child_object_type = $this->object_sub_type . '-revision';
  177. }
  178. } elseif ( $taxonomy = $this->get_taxonomy_by_rest_base( $base ) ) {
  179. $this->object_type = 'term';
  180. $this->object_sub_type = $taxonomy->name;
  181. }
  182. }
  183. /**
  184. * Find the REST enabled post type object that matches the given REST base.
  185. *
  186. * @param string $rest_base
  187. * @return WP_Post_Type|null
  188. */
  189. private function get_post_type_by_rest_base( $rest_base ) {
  190. $types = get_post_types( array( 'show_in_rest' => true ), 'objects' );
  191. foreach ( $types as $type ) {
  192. if ( acf_get_object_type_rest_base( $type ) === $rest_base ) {
  193. return $type;
  194. }
  195. }
  196. return null;
  197. }
  198. /**
  199. * Find the REST enabled taxonomy object that matches the given REST base.
  200. *
  201. * @param $rest_base
  202. * @return WP_Taxonomy|null
  203. */
  204. private function get_taxonomy_by_rest_base( $rest_base ) {
  205. $taxonomies = get_taxonomies( array( 'show_in_rest' => true ), 'objects' );
  206. foreach ( $taxonomies as $taxonomy ) {
  207. if ( acf_get_object_type_rest_base( $taxonomy ) === $rest_base ) {
  208. return $taxonomy;
  209. }
  210. }
  211. return null;
  212. }
  213. }