run:R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
25.9 KB
2026-03-11 16:18:52
R W Run
7.2 KB
2026-03-11 16:18:52
R W Run
56.67 KB
2026-03-11 16:18:52
R W Run
error_log
📄class-wp-rest-request.php
1<?php
2/**
3 * REST API: WP_REST_Request class
4 *
5 * @package WordPress
6 * @subpackage REST_API
7 * @since 4.4.0
8 */
9
10/**
11 * Core class used to implement a REST request object.
12 *
13 * Contains data from the request, to be passed to the callback.
14 *
15 * Note: This implements ArrayAccess, and acts as an array of parameters when
16 * used in that manner. It does not use ArrayObject (as we cannot rely on SPL),
17 * so be aware it may have non-array behavior in some cases.
18 *
19 * Note: When using features provided by ArrayAccess, be aware that WordPress deliberately
20 * does not distinguish between arguments of the same name for different request methods.
21 * For instance, in a request with `GET id=1` and `POST id=2`, `$request['id']` will equal
22 * 2 (`POST`) not 1 (`GET`). For more precision between request methods, use
23 * WP_REST_Request::get_body_params(), WP_REST_Request::get_url_params(), etc.
24 *
25 * @since 4.4.0
26 *
27 * @link https://www.php.net/manual/en/class.arrayaccess.php
28 */
29#[AllowDynamicProperties]
30class WP_REST_Request implements ArrayAccess {
31
32 /**
33 * HTTP method.
34 *
35 * @since 4.4.0
36 * @var string
37 */
38 protected $method = '';
39
40 /**
41 * Parameters passed to the request.
42 *
43 * These typically come from the `$_GET`, `$_POST` and `$_FILES`
44 * superglobals when being created from the global scope.
45 *
46 * @since 4.4.0
47 * @var array Contains GET, POST and FILES keys mapping to arrays of data.
48 */
49 protected $params;
50
51 /**
52 * HTTP headers for the request.
53 *
54 * @since 4.4.0
55 * @var array Map of key to value. Key is always lowercase, as per HTTP specification.
56 */
57 protected $headers = array();
58
59 /**
60 * Body data.
61 *
62 * @since 4.4.0
63 * @var string Binary data from the request.
64 */
65 protected $body = null;
66
67 /**
68 * Route matched for the request.
69 *
70 * @since 4.4.0
71 * @var string
72 */
73 protected $route;
74
75 /**
76 * Attributes (options) for the route that was matched.
77 *
78 * This is the options array used when the route was registered, typically
79 * containing the callback as well as the valid methods for the route.
80 *
81 * @since 4.4.0
82 * @var array Attributes for the request.
83 */
84 protected $attributes = array();
85
86 /**
87 * Used to determine if the JSON data has been parsed yet.
88 *
89 * Allows lazy-parsing of JSON data where possible.
90 *
91 * @since 4.4.0
92 * @var bool
93 */
94 protected $parsed_json = false;
95
96 /**
97 * Used to determine if the body data has been parsed yet.
98 *
99 * @since 4.4.0
100 * @var bool
101 */
102 protected $parsed_body = false;
103
104 /**
105 * Constructor.
106 *
107 * @since 4.4.0
108 *
109 * @param string $method Optional. Request method. Default empty.
110 * @param string $route Optional. Request route. Default empty.
111 * @param array $attributes Optional. Request attributes. Default empty array.
112 */
113 public function __construct( $method = '', $route = '', $attributes = array() ) {
114 $this->params = array(
115 'URL' => array(),
116 'GET' => array(),
117 'POST' => array(),
118 'FILES' => array(),
119
120 // See parse_json_params.
121 'JSON' => null,
122
123 'defaults' => array(),
124 );
125
126 $this->set_method( $method );
127 $this->set_route( $route );
128 $this->set_attributes( $attributes );
129 }
130
131 /**
132 * Retrieves the HTTP method for the request.
133 *
134 * @since 4.4.0
135 *
136 * @return string HTTP method.
137 */
138 public function get_method() {
139 return $this->method;
140 }
141
142 /**
143 * Sets HTTP method for the request.
144 *
145 * @since 4.4.0
146 *
147 * @param string $method HTTP method.
148 */
149 public function set_method( $method ) {
150 $this->method = strtoupper( $method );
151 }
152
153 /**
154 * Retrieves all headers from the request.
155 *
156 * @since 4.4.0
157 *
158 * @return array Map of key to value. Key is always lowercase, as per HTTP specification.
159 */
160 public function get_headers() {
161 return $this->headers;
162 }
163
164 /**
165 * Determines if the request is the given method.
166 *
167 * @since 6.8.0
168 *
169 * @param string $method HTTP method.
170 * @return bool Whether the request is of the given method.
171 */
172 public function is_method( $method ) {
173 return $this->get_method() === strtoupper( $method );
174 }
175
176 /**
177 * Canonicalizes the header name.
178 *
179 * Ensures that header names are always treated the same regardless of
180 * source. Header names are always case-insensitive.
181 *
182 * Note that we treat `-` (dashes) and `_` (underscores) as the same
183 * character, as per header parsing rules in both Apache and nginx.
184 *
185 * @link https://stackoverflow.com/q/18185366
186 * @link https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/#missing-disappearing-http-headers
187 * @link https://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers
188 *
189 * @since 4.4.0
190 *
191 * @param string $key Header name.
192 * @return string Canonicalized name.
193 */
194 public static function canonicalize_header_name( $key ) {
195 $key = strtolower( $key );
196 $key = str_replace( '-', '_', $key );
197
198 return $key;
199 }
200
201 /**
202 * Retrieves the given header from the request.
203 *
204 * If the header has multiple values, they will be concatenated with a comma
205 * as per the HTTP specification. Be aware that some non-compliant headers
206 * (notably cookie headers) cannot be joined this way.
207 *
208 * @since 4.4.0
209 *
210 * @param string $key Header name, will be canonicalized to lowercase.
211 * @return string|null String value if set, null otherwise.
212 */
213 public function get_header( $key ) {
214 $key = $this->canonicalize_header_name( $key );
215
216 if ( ! isset( $this->headers[ $key ] ) ) {
217 return null;
218 }
219
220 return implode( ',', $this->headers[ $key ] );
221 }
222
223 /**
224 * Retrieves header values from the request.
225 *
226 * @since 4.4.0
227 *
228 * @param string $key Header name, will be canonicalized to lowercase.
229 * @return array|null List of string values if set, null otherwise.
230 */
231 public function get_header_as_array( $key ) {
232 $key = $this->canonicalize_header_name( $key );
233
234 if ( ! isset( $this->headers[ $key ] ) ) {
235 return null;
236 }
237
238 return $this->headers[ $key ];
239 }
240
241 /**
242 * Sets the header on request.
243 *
244 * @since 4.4.0
245 *
246 * @param string $key Header name.
247 * @param string $value Header value, or list of values.
248 */
249 public function set_header( $key, $value ) {
250 $key = $this->canonicalize_header_name( $key );
251 $value = (array) $value;
252
253 $this->headers[ $key ] = $value;
254 }
255
256 /**
257 * Appends a header value for the given header.
258 *
259 * @since 4.4.0
260 *
261 * @param string $key Header name.
262 * @param string $value Header value, or list of values.
263 */
264 public function add_header( $key, $value ) {
265 $key = $this->canonicalize_header_name( $key );
266 $value = (array) $value;
267
268 if ( ! isset( $this->headers[ $key ] ) ) {
269 $this->headers[ $key ] = array();
270 }
271
272 $this->headers[ $key ] = array_merge( $this->headers[ $key ], $value );
273 }
274
275 /**
276 * Removes all values for a header.
277 *
278 * @since 4.4.0
279 *
280 * @param string $key Header name.
281 */
282 public function remove_header( $key ) {
283 $key = $this->canonicalize_header_name( $key );
284 unset( $this->headers[ $key ] );
285 }
286
287 /**
288 * Sets headers on the request.
289 *
290 * @since 4.4.0
291 *
292 * @param array $headers Map of header name to value.
293 * @param bool $override If true, replace the request's headers. Otherwise, merge with existing.
294 */
295 public function set_headers( $headers, $override = true ) {
296 if ( true === $override ) {
297 $this->headers = array();
298 }
299
300 foreach ( $headers as $key => $value ) {
301 $this->set_header( $key, $value );
302 }
303 }
304
305 /**
306 * Retrieves the Content-Type of the request.
307 *
308 * @since 4.4.0
309 *
310 * @return array|null Map containing 'value' and 'parameters' keys
311 * or null when no valid Content-Type header was
312 * available.
313 */
314 public function get_content_type() {
315 $value = $this->get_header( 'Content-Type' );
316 if ( empty( $value ) ) {
317 return null;
318 }
319
320 $parameters = '';
321 if ( strpos( $value, ';' ) ) {
322 list( $value, $parameters ) = explode( ';', $value, 2 );
323 }
324
325 $value = strtolower( $value );
326 if ( ! str_contains( $value, '/' ) ) {
327 return null;
328 }
329
330 // Parse type and subtype out.
331 list( $type, $subtype ) = explode( '/', $value, 2 );
332
333 $data = compact( 'value', 'type', 'subtype', 'parameters' );
334 $data = array_map( 'trim', $data );
335
336 return $data;
337 }
338
339 /**
340 * Checks if the request has specified a JSON Content-Type.
341 *
342 * @since 5.6.0
343 *
344 * @return bool True if the Content-Type header is JSON.
345 */
346 public function is_json_content_type() {
347 $content_type = $this->get_content_type();
348
349 return isset( $content_type['value'] ) && wp_is_json_media_type( $content_type['value'] );
350 }
351
352 /**
353 * Retrieves the parameter priority order.
354 *
355 * Used when checking parameters in WP_REST_Request::get_param().
356 *
357 * @since 4.4.0
358 *
359 * @return string[] Array of types to check, in order of priority.
360 */
361 protected function get_parameter_order() {
362 $order = array();
363
364 if ( $this->is_json_content_type() ) {
365 $order[] = 'JSON';
366 }
367
368 $this->parse_json_params();
369
370 // Ensure we parse the body data.
371 $body = $this->get_body();
372
373 if ( 'POST' !== $this->method && ! empty( $body ) ) {
374 $this->parse_body_params();
375 }
376
377 $accepts_body_data = array( 'POST', 'PUT', 'PATCH', 'DELETE' );
378 if ( in_array( $this->method, $accepts_body_data, true ) ) {
379 $order[] = 'POST';
380 }
381
382 $order[] = 'GET';
383 $order[] = 'URL';
384 $order[] = 'defaults';
385
386 /**
387 * Filters the parameter priority order for a REST API request.
388 *
389 * The order affects which parameters are checked when using WP_REST_Request::get_param()
390 * and family. This acts similarly to PHP's `request_order` setting.
391 *
392 * @since 4.4.0
393 *
394 * @param string[] $order Array of types to check, in order of priority.
395 * @param WP_REST_Request $request The request object.
396 */
397 return apply_filters( 'rest_request_parameter_order', $order, $this );
398 }
399
400 /**
401 * Retrieves a parameter from the request.
402 *
403 * @since 4.4.0
404 *
405 * @param string $key Parameter name.
406 * @return mixed|null Value if set, null otherwise.
407 */
408 public function get_param( $key ) {
409 $order = $this->get_parameter_order();
410
411 foreach ( $order as $type ) {
412 // Determine if we have the parameter for this type.
413 if ( isset( $this->params[ $type ][ $key ] ) ) {
414 return $this->params[ $type ][ $key ];
415 }
416 }
417
418 return null;
419 }
420
421 /**
422 * Checks if a parameter exists in the request.
423 *
424 * This allows distinguishing between an omitted parameter,
425 * and a parameter specifically set to null.
426 *
427 * @since 5.3.0
428 *
429 * @param string $key Parameter name.
430 * @return bool True if a param exists for the given key.
431 */
432 public function has_param( $key ) {
433 $order = $this->get_parameter_order();
434
435 foreach ( $order as $type ) {
436 if ( is_array( $this->params[ $type ] ) && array_key_exists( $key, $this->params[ $type ] ) ) {
437 return true;
438 }
439 }
440
441 return false;
442 }
443
444 /**
445 * Sets a parameter on the request.
446 *
447 * If the given parameter key exists in any parameter type an update will take place,
448 * otherwise a new param will be created in the first parameter type (respecting
449 * get_parameter_order()).
450 *
451 * @since 4.4.0
452 *
453 * @param string $key Parameter name.
454 * @param mixed $value Parameter value.
455 */
456 public function set_param( $key, $value ) {
457 $order = $this->get_parameter_order();
458 $found_key = false;
459
460 foreach ( $order as $type ) {
461 if ( 'defaults' !== $type && is_array( $this->params[ $type ] ) && array_key_exists( $key, $this->params[ $type ] ) ) {
462 $this->params[ $type ][ $key ] = $value;
463 $found_key = true;
464 }
465 }
466
467 if ( ! $found_key ) {
468 $this->params[ $order[0] ][ $key ] = $value;
469 }
470 }
471
472 /**
473 * Retrieves merged parameters from the request.
474 *
475 * The equivalent of get_param(), but returns all parameters for the request.
476 * Handles merging all the available values into a single array.
477 *
478 * @since 4.4.0
479 *
480 * @return array Map of key to value.
481 */
482 public function get_params() {
483 $order = $this->get_parameter_order();
484 $order = array_reverse( $order, true );
485
486 $params = array();
487 foreach ( $order as $type ) {
488 /*
489 * array_merge() / the "+" operator will mess up
490 * numeric keys, so instead do a manual foreach.
491 */
492 foreach ( (array) $this->params[ $type ] as $key => $value ) {
493 $params[ $key ] = $value;
494 }
495 }
496
497 // Exclude rest_route if pretty permalinks are not enabled.
498 if ( ! get_option( 'permalink_structure' ) ) {
499 unset( $params['rest_route'] );
500 }
501
502 return $params;
503 }
504
505 /**
506 * Retrieves parameters from the route itself.
507 *
508 * These are parsed from the URL using the regex.
509 *
510 * @since 4.4.0
511 *
512 * @return array Parameter map of key to value.
513 */
514 public function get_url_params() {
515 return $this->params['URL'];
516 }
517
518 /**
519 * Sets parameters from the route.
520 *
521 * Typically, this is set after parsing the URL.
522 *
523 * @since 4.4.0
524 *
525 * @param array $params Parameter map of key to value.
526 */
527 public function set_url_params( $params ) {
528 $this->params['URL'] = $params;
529 }
530
531 /**
532 * Retrieves parameters from the query string.
533 *
534 * These are the parameters you'd typically find in `$_GET`.
535 *
536 * @since 4.4.0
537 *
538 * @return array Parameter map of key to value.
539 */
540 public function get_query_params() {
541 return $this->params['GET'];
542 }
543
544 /**
545 * Sets parameters from the query string.
546 *
547 * Typically, this is set from `$_GET`.
548 *
549 * @since 4.4.0
550 *
551 * @param array $params Parameter map of key to value.
552 */
553 public function set_query_params( $params ) {
554 $this->params['GET'] = $params;
555 }
556
557 /**
558 * Retrieves parameters from the body.
559 *
560 * These are the parameters you'd typically find in `$_POST`.
561 *
562 * @since 4.4.0
563 *
564 * @return array Parameter map of key to value.
565 */
566 public function get_body_params() {
567 return $this->params['POST'];
568 }
569
570 /**
571 * Sets parameters from the body.
572 *
573 * Typically, this is set from `$_POST`.
574 *
575 * @since 4.4.0
576 *
577 * @param array $params Parameter map of key to value.
578 */
579 public function set_body_params( $params ) {
580 $this->params['POST'] = $params;
581 }
582
583 /**
584 * Retrieves multipart file parameters from the body.
585 *
586 * These are the parameters you'd typically find in `$_FILES`.
587 *
588 * @since 4.4.0
589 *
590 * @return array Parameter map of key to value.
591 */
592 public function get_file_params() {
593 return $this->params['FILES'];
594 }
595
596 /**
597 * Sets multipart file parameters from the body.
598 *
599 * Typically, this is set from `$_FILES`.
600 *
601 * @since 4.4.0
602 *
603 * @param array $params Parameter map of key to value.
604 */
605 public function set_file_params( $params ) {
606 $this->params['FILES'] = $params;
607 }
608
609 /**
610 * Retrieves the default parameters.
611 *
612 * These are the parameters set in the route registration.
613 *
614 * @since 4.4.0
615 *
616 * @return array Parameter map of key to value.
617 */
618 public function get_default_params() {
619 return $this->params['defaults'];
620 }
621
622 /**
623 * Sets default parameters.
624 *
625 * These are the parameters set in the route registration.
626 *
627 * @since 4.4.0
628 *
629 * @param array $params Parameter map of key to value.
630 */
631 public function set_default_params( $params ) {
632 $this->params['defaults'] = $params;
633 }
634
635 /**
636 * Retrieves the request body content.
637 *
638 * @since 4.4.0
639 *
640 * @return string Binary data from the request body.
641 */
642 public function get_body() {
643 return $this->body;
644 }
645
646 /**
647 * Sets body content.
648 *
649 * @since 4.4.0
650 *
651 * @param string $data Binary data from the request body.
652 */
653 public function set_body( $data ) {
654 $this->body = $data;
655
656 // Enable lazy parsing.
657 $this->parsed_json = false;
658 $this->parsed_body = false;
659 $this->params['JSON'] = null;
660 }
661
662 /**
663 * Retrieves the parameters from a JSON-formatted body.
664 *
665 * @since 4.4.0
666 *
667 * @return array Parameter map of key to value.
668 */
669 public function get_json_params() {
670 // Ensure the parameters have been parsed out.
671 $this->parse_json_params();
672
673 return $this->params['JSON'];
674 }
675
676 /**
677 * Parses the JSON parameters.
678 *
679 * Avoids parsing the JSON data until we need to access it.
680 *
681 * @since 4.4.0
682 * @since 4.7.0 Returns error instance if value cannot be decoded.
683 * @return true|WP_Error True if the JSON data was passed or no JSON data was provided, WP_Error if invalid JSON was passed.
684 */
685 protected function parse_json_params() {
686 if ( $this->parsed_json ) {
687 return true;
688 }
689
690 $this->parsed_json = true;
691
692 // Check that we actually got JSON.
693 if ( ! $this->is_json_content_type() ) {
694 return true;
695 }
696
697 $body = $this->get_body();
698 if ( empty( $body ) ) {
699 return true;
700 }
701
702 $params = json_decode( $body, true );
703
704 /*
705 * Check for a parsing error.
706 */
707 if ( null === $params && JSON_ERROR_NONE !== json_last_error() ) {
708 // Ensure subsequent calls receive error instance.
709 $this->parsed_json = false;
710
711 $error_data = array(
712 'status' => WP_Http::BAD_REQUEST,
713 'json_error_code' => json_last_error(),
714 'json_error_message' => json_last_error_msg(),
715 );
716
717 return new WP_Error( 'rest_invalid_json', __( 'Invalid JSON body passed.' ), $error_data );
718 }
719
720 $this->params['JSON'] = $params;
721
722 return true;
723 }
724
725 /**
726 * Parses the request body parameters.
727 *
728 * Parses out URL-encoded bodies for request methods that aren't supported
729 * natively by PHP.
730 *
731 * @since 4.4.0
732 */
733 protected function parse_body_params() {
734 if ( $this->parsed_body ) {
735 return;
736 }
737
738 $this->parsed_body = true;
739
740 /*
741 * Check that we got URL-encoded. Treat a missing Content-Type as
742 * URL-encoded for maximum compatibility.
743 */
744 $content_type = $this->get_content_type();
745
746 if ( ! empty( $content_type ) && 'application/x-www-form-urlencoded' !== $content_type['value'] ) {
747 return;
748 }
749
750 parse_str( $this->get_body(), $params );
751
752 /*
753 * Add to the POST parameters stored internally. If a user has already
754 * set these manually (via `set_body_params`), don't override them.
755 */
756 $this->params['POST'] = array_merge( $params, $this->params['POST'] );
757 }
758
759 /**
760 * Retrieves the route that matched the request.
761 *
762 * @since 4.4.0
763 *
764 * @return string Route matching regex.
765 */
766 public function get_route() {
767 return $this->route;
768 }
769
770 /**
771 * Sets the route that matched the request.
772 *
773 * @since 4.4.0
774 *
775 * @param string $route Route matching regex.
776 */
777 public function set_route( $route ) {
778 $this->route = $route;
779 }
780
781 /**
782 * Retrieves the attributes for the request.
783 *
784 * These are the options for the route that was matched.
785 *
786 * @since 4.4.0
787 *
788 * @return array Attributes for the request.
789 */
790 public function get_attributes() {
791 return $this->attributes;
792 }
793
794 /**
795 * Sets the attributes for the request.
796 *
797 * @since 4.4.0
798 *
799 * @param array $attributes Attributes for the request.
800 */
801 public function set_attributes( $attributes ) {
802 $this->attributes = $attributes;
803 }
804
805 /**
806 * Sanitizes (where possible) the params on the request.
807 *
808 * This is primarily based off the sanitize_callback param on each registered
809 * argument.
810 *
811 * @since 4.4.0
812 *
813 * @return true|WP_Error True if parameters were sanitized, WP_Error if an error occurred during sanitization.
814 */
815 public function sanitize_params() {
816 $attributes = $this->get_attributes();
817
818 // No arguments set, skip sanitizing.
819 if ( empty( $attributes['args'] ) ) {
820 return true;
821 }
822
823 $order = $this->get_parameter_order();
824
825 $invalid_params = array();
826 $invalid_details = array();
827
828 foreach ( $order as $type ) {
829 if ( empty( $this->params[ $type ] ) ) {
830 continue;
831 }
832
833 foreach ( $this->params[ $type ] as $key => $value ) {
834 if ( ! isset( $attributes['args'][ $key ] ) ) {
835 continue;
836 }
837
838 $param_args = $attributes['args'][ $key ];
839
840 // If the arg has a type but no sanitize_callback attribute, default to rest_parse_request_arg.
841 if ( ! array_key_exists( 'sanitize_callback', $param_args ) && ! empty( $param_args['type'] ) ) {
842 $param_args['sanitize_callback'] = 'rest_parse_request_arg';
843 }
844 // If there's still no sanitize_callback, nothing to do here.
845 if ( empty( $param_args['sanitize_callback'] ) ) {
846 continue;
847 }
848
849 /** @var mixed|WP_Error $sanitized_value */
850 $sanitized_value = call_user_func( $param_args['sanitize_callback'], $value, $this, $key );
851
852 if ( is_wp_error( $sanitized_value ) ) {
853 $invalid_params[ $key ] = implode( ' ', $sanitized_value->get_error_messages() );
854 $invalid_details[ $key ] = rest_convert_error_to_response( $sanitized_value )->get_data();
855 } else {
856 $this->params[ $type ][ $key ] = $sanitized_value;
857 }
858 }
859 }
860
861 if ( $invalid_params ) {
862 return new WP_Error(
863 'rest_invalid_param',
864 /* translators: %s: List of invalid parameters. */
865 sprintf( __( 'Invalid parameter(s): %s' ), implode( ', ', array_keys( $invalid_params ) ) ),
866 array(
867 'status' => 400,
868 'params' => $invalid_params,
869 'details' => $invalid_details,
870 )
871 );
872 }
873
874 return true;
875 }
876
877 /**
878 * Checks whether this request is valid according to its attributes.
879 *
880 * @since 4.4.0
881 *
882 * @return true|WP_Error True if there are no parameters to validate or if all pass validation,
883 * WP_Error if required parameters are missing.
884 */
885 public function has_valid_params() {
886 // If JSON data was passed, check for errors.
887 $json_error = $this->parse_json_params();
888 if ( is_wp_error( $json_error ) ) {
889 return $json_error;
890 }
891
892 $attributes = $this->get_attributes();
893 $required = array();
894
895 $args = empty( $attributes['args'] ) ? array() : $attributes['args'];
896
897 foreach ( $args as $key => $arg ) {
898 $param = $this->get_param( $key );
899 if ( isset( $arg['required'] ) && true === $arg['required'] && null === $param ) {
900 $required[] = $key;
901 }
902 }
903
904 if ( ! empty( $required ) ) {
905 return new WP_Error(
906 'rest_missing_callback_param',
907 /* translators: %s: List of required parameters. */
908 sprintf( __( 'Missing parameter(s): %s' ), implode( ', ', $required ) ),
909 array(
910 'status' => 400,
911 'params' => $required,
912 )
913 );
914 }
915
916 /*
917 * Check the validation callbacks for each registered arg.
918 *
919 * This is done after required checking as required checking is cheaper.
920 */
921 $invalid_params = array();
922 $invalid_details = array();
923
924 foreach ( $args as $key => $arg ) {
925
926 $param = $this->get_param( $key );
927
928 if ( null !== $param && ! empty( $arg['validate_callback'] ) ) {
929 /** @var bool|\WP_Error $valid_check */
930 $valid_check = call_user_func( $arg['validate_callback'], $param, $this, $key );
931
932 if ( false === $valid_check ) {
933 $invalid_params[ $key ] = __( 'Invalid parameter.' );
934 }
935
936 if ( is_wp_error( $valid_check ) ) {
937 $invalid_params[ $key ] = implode( ' ', $valid_check->get_error_messages() );
938 $invalid_details[ $key ] = rest_convert_error_to_response( $valid_check )->get_data();
939 }
940 }
941 }
942
943 if ( $invalid_params ) {
944 return new WP_Error(
945 'rest_invalid_param',
946 /* translators: %s: List of invalid parameters. */
947 sprintf( __( 'Invalid parameter(s): %s' ), implode( ', ', array_keys( $invalid_params ) ) ),
948 array(
949 'status' => 400,
950 'params' => $invalid_params,
951 'details' => $invalid_details,
952 )
953 );
954 }
955
956 if ( isset( $attributes['validate_callback'] ) ) {
957 $valid_check = call_user_func( $attributes['validate_callback'], $this );
958
959 if ( is_wp_error( $valid_check ) ) {
960 return $valid_check;
961 }
962
963 if ( false === $valid_check ) {
964 // A WP_Error instance is preferred, but false is supported for parity with the per-arg validate_callback.
965 return new WP_Error( 'rest_invalid_params', __( 'Invalid parameters.' ), array( 'status' => 400 ) );
966 }
967 }
968
969 return true;
970 }
971
972 /**
973 * Checks if a parameter is set.
974 *
975 * @since 4.4.0
976 *
977 * @param string $offset Parameter name.
978 * @return bool Whether the parameter is set.
979 */
980 #[ReturnTypeWillChange]
981 public function offsetExists( $offset ) {
982 $order = $this->get_parameter_order();
983
984 foreach ( $order as $type ) {
985 if ( isset( $this->params[ $type ][ $offset ] ) ) {
986 return true;
987 }
988 }
989
990 return false;
991 }
992
993 /**
994 * Retrieves a parameter from the request.
995 *
996 * @since 4.4.0
997 *
998 * @param string $offset Parameter name.
999 * @return mixed|null Value if set, null otherwise.
1000 */
1001 #[ReturnTypeWillChange]
1002 public function offsetGet( $offset ) {
1003 return $this->get_param( $offset );
1004 }
1005
1006 /**
1007 * Sets a parameter on the request.
1008 *
1009 * @since 4.4.0
1010 *
1011 * @param string $offset Parameter name.
1012 * @param mixed $value Parameter value.
1013 */
1014 #[ReturnTypeWillChange]
1015 public function offsetSet( $offset, $value ) {
1016 $this->set_param( $offset, $value );
1017 }
1018
1019 /**
1020 * Removes a parameter from the request.
1021 *
1022 * @since 4.4.0
1023 *
1024 * @param string $offset Parameter name.
1025 */
1026 #[ReturnTypeWillChange]
1027 public function offsetUnset( $offset ) {
1028 $order = $this->get_parameter_order();
1029
1030 // Remove the offset from every group.
1031 foreach ( $order as $type ) {
1032 unset( $this->params[ $type ][ $offset ] );
1033 }
1034 }
1035
1036 /**
1037 * Retrieves a WP_REST_Request object from a full URL.
1038 *
1039 * @since 4.5.0
1040 *
1041 * @param string $url URL with protocol, domain, path and query args.
1042 * @return WP_REST_Request|false WP_REST_Request object on success, false on failure.
1043 */
1044 public static function from_url( $url ) {
1045 $bits = parse_url( $url );
1046 $query_params = array();
1047
1048 if ( ! empty( $bits['query'] ) ) {
1049 wp_parse_str( $bits['query'], $query_params );
1050 }
1051
1052 $api_root = rest_url();
1053 if ( get_option( 'permalink_structure' ) && str_starts_with( $url, $api_root ) ) {
1054 // Pretty permalinks on, and URL is under the API root.
1055 $api_url_part = substr( $url, strlen( untrailingslashit( $api_root ) ) );
1056 $route = parse_url( $api_url_part, PHP_URL_PATH );
1057 } elseif ( ! empty( $query_params['rest_route'] ) ) {
1058 // ?rest_route=... set directly.
1059 $route = $query_params['rest_route'];
1060 unset( $query_params['rest_route'] );
1061 }
1062
1063 $request = false;
1064 if ( ! empty( $route ) ) {
1065 $request = new WP_REST_Request( 'GET', $route );
1066 $request->set_query_params( $query_params );
1067 }
1068
1069 /**
1070 * Filters the REST API request generated from a URL.
1071 *
1072 * @since 4.5.0
1073 *
1074 * @param WP_REST_Request|false $request Generated request object, or false if URL
1075 * could not be parsed.
1076 * @param string $url URL the request was generated from.
1077 */
1078 return apply_filters( 'rest_request_from_url', $request, $url );
1079 }
1080}
1081