PluginProbe ʕ •ᴥ•ʔ
Kirki – Freeform Page Builder, Website Builder & Customizer / 6.2.4
Kirki – Freeform Page Builder, Website Builder & Customizer v6.2.4
6.2.5 6.2.4 6.2.3 6.2.2 6.2.1 6.2.0 6.1.1 6.1.0 6.0.14 6.0.13 6.0.12 6.0.11 6.0.10 6.0.9 6.0.8 6.0.7 6.0.6 6.0.5 6.0.4 6.0.3 6.0.2 6.0.1 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 4.0.19 4.0.20 4.0.21 4.0.22 4.0.23 4.0.24 4.1 4.2.0 5.0.0 5.1.0 5.1.1 5.2.0 5.2.1 5.2.2 5.2.3 6.0.0 trunk 3.0.40 3.0.41 3.0.42 3.0.43 3.0.44 3.0.45 3.1.0 3.1.1 3.1.2
kirki / libraries / framework / Http / Client / Request.php
kirki / libraries / framework / Http / Client Last commit date
MultipartStream.php 1 month ago Request.php 1 week ago Response.php 1 month ago
Request.php
772 lines
1 <?php
2
3 /**
4 * Fluent HTTP client built on wp_remote_request with chainable headers, body, and auth options.
5 * Supports GET, POST, PUT, PATCH, DELETE and multipart or JSON body formats.
6 * Macroable for extending with custom request helpers.
7 *
8 * @package Framework
9 * @subpackage Http\Client
10 * @since 1.0.0
11 */
12 namespace Kirki\Framework\Http\Client;
13
14 \defined('ABSPATH') || exit;
15 use BadMethodCallException;
16 use Kirki\Framework\Http\Cookie;
17 use Kirki\Framework\Supports\Arr;
18 use Kirki\Framework\Supports\Str;
19 use Kirki\Framework\Supports\Traits\Macroable;
20 use RuntimeException;
21 use WP_Http_Cookie;
22 use function Kirki\Framework\collection;
23 use function Kirki\Framework\Polyfill\str_contains;
24 class Request
25 {
26 /**
27 * The macroable trait.
28 *
29 * @var Macroable
30 */
31 use Macroable {
32 __call as call_macro;
33 }
34 /**
35 * HTTP GET method.
36 */
37 public const GET = 'GET';
38 /**
39 * HTTP POST method.
40 */
41 public const POST = 'POST';
42 /**
43 * HTTP PUT method.
44 */
45 public const PUT = 'PUT';
46 /**
47 * HTTP PATCH method.
48 */
49 public const PATCH = 'PATCH';
50 /**
51 * HTTP DELETE method.
52 */
53 public const DELETE = 'DELETE';
54 /**
55 * HTTP HEAD method.
56 */
57 public const HEAD = 'HEAD';
58 /**
59 * HTTP OPTIONS method.
60 */
61 public const OPTIONS = 'OPTIONS';
62 /**
63 * The base URL for the request.
64 *
65 * @var string
66 *
67 * @since 1.0.0
68 */
69 protected string $base_url = '';
70 /**
71 * The options for the request.
72 *
73 * @var array
74 *
75 * @since 1.0.0
76 */
77 protected array $options = [];
78 /**
79 * The body format for the request.
80 *
81 * @var string
82 *
83 * @since 1.0.0
84 */
85 protected string $body_format;
86 /**
87 * The content for the request.
88 *
89 * @var mixed
90 *
91 * @since 1.0.0
92 */
93 protected $content = null;
94 /**
95 * The files for the request.
96 *
97 * @var array
98 *
99 * @since 1.0.0
100 */
101 protected $files = [];
102 /**
103 * The HTTP methods for the request.
104 *
105 * @var array
106 *
107 * @since 1.0.0
108 */
109 protected array $methods = [self::GET, self::POST, self::PUT, self::PATCH, self::DELETE, self::HEAD, self::OPTIONS];
110 /**
111 * The constructor.
112 *
113 * @return void
114 *
115 * @since 1.0.0
116 */
117 public function __construct()
118 {
119 $this->options = ['method' => 'GET', 'timeout' => 30, 'redirection' => 5, 'httpversion' => '1.0', 'reject_unsafe_url' => \false, 'blocking' => \true, 'cookies' => [], 'body' => null, 'compress' => \false, 'decompress' => \true, 'sslverify' => \true, 'stream' => \false, 'filename' => null, 'limit_response_size' => null, 'headers' => []];
120 $this->as_json();
121 }
122 /**
123 * Set the base URL for the request.
124 *
125 * @param string $url The url.
126 *
127 * @return $this
128 *
129 * @since 1.0.0
130 */
131 public function base_url(string $url)
132 {
133 $this->base_url = $url;
134 return $this;
135 }
136 /**
137 * Get the base URL for the request.
138 *
139 * @return string
140 *
141 * @since 1.0.0
142 */
143 public function get_base_url()
144 {
145 return $this->base_url;
146 }
147 /**
148 * Set the headers for the request.
149 *
150 * @param array $headers The headers.
151 *
152 * @return $this
153 *
154 * @since 1.0.0
155 */
156 public function with_headers(array $headers)
157 {
158 $this->options = \array_merge_recursive($this->options, ['headers' => $headers]);
159 return $this;
160 }
161 /**
162 * Replace the headers for the request.
163 *
164 * @param array $headers The headers.
165 *
166 * @return $this
167 *
168 * @since 1.0.0
169 */
170 public function replace_headers(array $headers)
171 {
172 $this->options['headers'] = \array_merge($this->options['headers'] ?? [], $headers);
173 return $this;
174 }
175 /**
176 * Set the token for the request.
177 *
178 * @param string $token The token.
179 * @param mixed $type The type.
180 *
181 * @return $this
182 *
183 * @since 1.0.0
184 */
185 public function with_token(string $token, $type = 'Bearer')
186 {
187 return $this->with_headers(['Authorization' => \trim($type . ' ' . $token)]);
188 }
189 /**
190 * Set the user agent for the request.
191 *
192 * @param mixed $user_agent The user agent.
193 *
194 * @return $this
195 *
196 * @since 1.0.0
197 */
198 public function with_user_agent($user_agent)
199 {
200 return $this->with_headers(['User-Agent' => \trim($user_agent)]);
201 }
202 /**
203 * Attach a cookie to the request.
204 *
205 * @param string $name The name of the cookie.
206 * @param string $value The value of the cookie.
207 * @param string|null $domain The domain the cookie is scoped to.
208 *
209 * @return $this
210 *
211 * @since 1.0.0
212 */
213 public function with_cookie(string $name, string $value, ?string $domain = null)
214 {
215 $this->options['cookies'][$name] = $this->normalize_cookie($name, $value, $domain);
216 return $this;
217 }
218 /**
219 * Attach several cookies to the request.
220 *
221 * Accepts name and value pairs, Cookie instances, or WP_Http_Cookie instances.
222 *
223 * @param array $cookies The cookies to attach.
224 * @param string|null $domain The domain the cookies are scoped to.
225 *
226 * @return $this
227 *
228 * @since 1.0.0
229 */
230 public function with_cookies(array $cookies, ?string $domain = null)
231 {
232 foreach ($cookies as $name => $cookie) {
233 $normalized = $this->normalize_cookie($name, $cookie, $domain);
234 $this->options['cookies'][$normalized->name] = $normalized;
235 }
236 return $this;
237 }
238 /**
239 * Normalize a cookie of any supported shape into a WP_Http_Cookie instance.
240 *
241 * @param string|int $name The cookie name, when the value is a plain string.
242 * @param mixed $cookie The cookie value, Cookie instance, or WP_Http_Cookie instance.
243 * @param string|null $domain The domain the cookie is scoped to.
244 *
245 * @return \WP_Http_Cookie
246 *
247 * @since 1.0.0
248 */
249 protected function normalize_cookie($name, $cookie, ?string $domain = null)
250 {
251 if ($cookie instanceof WP_Http_Cookie) {
252 return $cookie;
253 }
254 if ($cookie instanceof Cookie) {
255 return new WP_Http_Cookie(['name' => $cookie->get_name(), 'value' => $cookie->get_value(), 'expires' => $cookie->get_expires_time() ?: null, 'path' => $cookie->get_path(), 'domain' => $cookie->get_domain() ?? $domain]);
256 }
257 return new WP_Http_Cookie(['name' => (string) $name, 'value' => (string) $cookie, 'domain' => $domain]);
258 }
259 /**
260 * Disable SSL verification for the request.
261 *
262 * @return $this
263 *
264 * @since 1.0.0
265 */
266 public function without_verifying()
267 {
268 $this->options['sslverify'] = \false;
269 return $this;
270 }
271 /**
272 * Set the method for the request.
273 *
274 * @param mixed $method The method name.
275 *
276 * @return $this
277 *
278 * @since 1.0.0
279 */
280 public function method($method)
281 {
282 $this->options['method'] = $method;
283 return $this;
284 }
285 /**
286 * Set the accept header for the request.
287 *
288 * @param mixed $value The value.
289 *
290 * @return $this
291 *
292 * @since 1.0.0
293 */
294 public function accept($value)
295 {
296 return $this->with_headers(['Accept' => $value]);
297 }
298 /**
299 * Set the accept header for the request to JSON.
300 *
301 * @return $this
302 *
303 * @since 1.0.0
304 */
305 public function accept_json()
306 {
307 return $this->accept('application/json');
308 }
309 /**
310 * Set the body format for the request.
311 *
312 * @param string $format The format.
313 *
314 * @return $this
315 *
316 * @since 1.0.0
317 */
318 public function body_format(string $format)
319 {
320 $this->body_format = $format;
321 return $this;
322 }
323 /**
324 * Set the body for the request.
325 *
326 * @param mixed $content The content.
327 * @param mixed $type The type.
328 *
329 * @return $this
330 *
331 * @since 1.0.0
332 */
333 public function with_body($content, $type = 'application/json')
334 {
335 $this->body_format = 'body';
336 $this->content = $content;
337 $this->content_type($type);
338 return $this;
339 }
340 /**
341 * Set the body format for the request to JSON.
342 *
343 * @return $this
344 *
345 * @since 1.0.0
346 */
347 public function as_json()
348 {
349 return $this->body_format('json')->content_type('application/json');
350 }
351 /**
352 * Set the body format for the request to form.
353 *
354 * @return $this
355 *
356 * @since 1.0.0
357 */
358 public function as_form()
359 {
360 return $this->body_format('form')->content_type('application/x-www-form-urlencoded');
361 }
362 /**
363 * Set the body format for the request to multipart.
364 *
365 * @return $this
366 *
367 * @since 1.0.0
368 */
369 public function as_multipart()
370 {
371 return $this->body_format('multipart')->content_type('multipart/form-data');
372 }
373 /**
374 * Attach a file to the request.
375 *
376 * @param mixed $name The name.
377 * @param mixed $contents The contents.
378 * @param mixed $filename The filename.
379 * @param array $headers The headers.
380 *
381 * @return $this
382 *
383 * @since 1.0.0
384 */
385 public function attach($name, $contents = '', $filename = null, array $headers = [])
386 {
387 if (\is_array($name)) {
388 foreach ($name as $file) {
389 $this->attach(...$file);
390 }
391 return $this;
392 }
393 $this->as_multipart();
394 $this->files[] = \array_filter(['name' => $name, 'contents' => $contents, 'headers' => $headers, 'filename' => $filename]);
395 return $this;
396 }
397 /**
398 * Set the content type for the request.
399 *
400 * @param mixed $type The type.
401 *
402 * @return $this
403 *
404 * @since 1.0.0
405 */
406 public function content_type($type)
407 {
408 $this->options['headers'] ??= [];
409 $this->options['headers']['Content-Type'] = $type;
410 return $this;
411 }
412 /**
413 * Set the timeout for the request.
414 *
415 * @param mixed $seconds The seconds.
416 *
417 * @return $this
418 *
419 * @since 1.0.0
420 */
421 public function timeout($seconds)
422 {
423 $this->options['timeout'] = $seconds;
424 return $this;
425 }
426 /**
427 * Set the options for the request.
428 *
429 * @param array $options The options array.
430 *
431 * @return $this
432 *
433 * @since 1.0.0
434 */
435 public function with_options(array $options)
436 {
437 $this->options = \array_replace_recursive(\array_merge_recursive($this->options, Arr::only($options, ['headers', 'json', 'multipart', 'query', 'cookies'])), $options);
438 return $this;
439 }
440 /**
441 * Set the body for the request.
442 *
443 * @param mixed $data The data payload.
444 *
445 * @return $this
446 *
447 * @since 1.0.0
448 */
449 protected function with_data($data)
450 {
451 $this->options['body'] = $data;
452 return $this;
453 }
454 /**
455 * Send a GET request.
456 *
457 * @param string $url The url.
458 * @param mixed $query The query builder instance.
459 *
460 * @return Response
461 *
462 * @since 1.0.0
463 */
464 public function get(string $url, $query = null)
465 {
466 return $this->send('GET', $url, \func_num_args() === 1 ? [] : ['query' => $query]);
467 }
468 /**
469 * Send a HEAD request.
470 *
471 * @param string $url The url.
472 * @param mixed $query The query builder instance.
473 *
474 * @return Response
475 *
476 * @since 1.0.0
477 */
478 public function head(string $url, $query = null)
479 {
480 return $this->send('HEAD', $url, \func_num_args() === 1 ? [] : ['query' => $query]);
481 }
482 /**
483 * Send a POST request.
484 *
485 * @param string $url The url.
486 * @param mixed $data The data payload.
487 *
488 * @return Response
489 *
490 * @since 1.0.0
491 */
492 public function post(string $url, $data = [])
493 {
494 return $this->send('POST', $url, \func_num_args() === 1 ? [] : [$this->body_format => $data]);
495 }
496 /**
497 * Send a PUT request.
498 *
499 * @param string $url The url.
500 * @param mixed $data The data payload.
501 *
502 * @return Response
503 *
504 * @since 1.0.0
505 */
506 public function put(string $url, $data = [])
507 {
508 return $this->send('PUT', $url, \func_num_args() === 1 ? [] : [$this->body_format => $data]);
509 }
510 /**
511 * Send a PATCH request.
512 *
513 * @param string $url The url.
514 * @param mixed $data The data payload.
515 *
516 * @return Response
517 *
518 * @since 1.0.0
519 */
520 public function patch(string $url, $data = [])
521 {
522 return $this->send('PATCH', $url, \func_num_args() === 1 ? [] : [$this->body_format => $data]);
523 }
524 /**
525 * Send a DELETE request.
526 *
527 * @param string $url The url.
528 * @param mixed $data The data payload.
529 *
530 * @return Response
531 *
532 * @since 1.0.0
533 */
534 public function delete(string $url, $data = [])
535 {
536 return $this->send('DELETE', $url, \func_num_args() === 1 ? [] : [$this->body_format => $data]);
537 }
538 /**
539 * Send an OPTIONS request.
540 *
541 * @param string $url The url.
542 * @param mixed $data The data payload.
543 *
544 * @return Response
545 *
546 * @since 1.0.0
547 */
548 public function options(string $url, $data = [])
549 {
550 return $this->send('OPTIONS', $url, \func_num_args() === 1 ? [] : [$this->body_format => $data]);
551 }
552 /**
553 * Send a request.
554 *
555 * @param string $method The method name.
556 * @param string $url The url.
557 * @param array $options The options array.
558 *
559 * @return Response
560 *
561 * @since 1.0.0
562 */
563 public function send(string $method, string $url, array $options = [])
564 {
565 if (!Str::starts_with($url, ['http', 'https'])) {
566 $url = \ltrim(\rtrim($this->base_url, '/') . '/' . \ltrim($url, '/'), '/');
567 }
568 $this->method($method);
569 return $this->send_request($method, $url, $options);
570 }
571 /**
572 * Send a request.
573 *
574 * @param string $method The method name.
575 * @param string $url The url.
576 * @param array $options The options array.
577 *
578 * @return Response
579 *
580 * @throws \RuntimeException
581 *
582 * @since 1.0.0
583 */
584 protected function send_request(string $method, string $url, array $options = [])
585 {
586 if (!$this->is_valid_method($method)) {
587 throw new RuntimeException(\sprintf('Invalid HTTP method: %s', $method));
588 }
589 $data = $this->parse_request_data($method, $url, $options);
590 $url = $this->prepare_request_url($method, $url, $data);
591 $body = $this->prepare_request_body($method, $data);
592 $args = $this->prepare_request_args($body);
593 $response = wp_remote_request($url, $args);
594 return $this->new_response($response);
595 }
596 /**
597 * Prepare request url.
598 *
599 * @param string $method The method name.
600 * @param string $url The url.
601 * @param array $data The data payload.
602 *
603 * @return void
604 *
605 * @since 1.0.0
606 */
607 protected function prepare_request_url(string $method, string $url, array $data)
608 {
609 if (!\in_array($method, ['GET', 'HEAD'])) {
610 return $url;
611 }
612 if (empty($data)) {
613 return $url;
614 }
615 return $url . '?' . \http_build_query($data);
616 }
617 /**
618 * Create a new response instance.
619 *
620 * @param mixed $response The response instance.
621 *
622 * @return Response
623 *
624 * @since 1.0.0
625 */
626 protected function new_response($response)
627 {
628 return new Response($response);
629 }
630 /**
631 * Parse the request data.
632 *
633 * @param string $method The method name.
634 * @param string $url The url.
635 * @param array $options The options array.
636 *
637 * @return array
638 *
639 * @since 1.0.0
640 */
641 protected function parse_request_data(string $method, string $url, array $options = [])
642 {
643 if ($this->body_format === 'body') {
644 return [];
645 }
646 $request_data = $options[$this->body_format] ?? $options['query'] ?? [];
647 if (empty($request_data) && $method === 'GET' && str_contains($url, '?')) {
648 $request_data = \substr($url, \strpos($url, '?') + 1);
649 }
650 if (\is_string($request_data)) {
651 \parse_str($request_data, $parsed_data);
652 $request_data = \is_array($parsed_data) ? $parsed_data : [];
653 }
654 return \is_array($request_data) ? $request_data : [];
655 }
656 /**
657 * Prepare the request body.
658 *
659 * @param string $method The method name.
660 * @param array $data The data payload.
661 *
662 * @return string|array
663 *
664 * @throws \RuntimeException
665 *
666 * @since 1.0.0
667 */
668 protected function prepare_request_body(string $method, array $data)
669 {
670 if (\in_array($method, ['GET', 'HEAD'])) {
671 return [];
672 }
673 switch ($this->body_format) {
674 case 'json':
675 return empty($data) ? '' : Arr::json_encode($data);
676 case 'body':
677 return $this->content ?? [];
678 case 'form':
679 return $data;
680 case 'multipart':
681 return $this->make_multipart_body($data);
682 default:
683 throw new RuntimeException(\sprintf('Invalid body format: %s', $this->body_format));
684 }
685 }
686 /**
687 * Make a multipart body.
688 *
689 * @param array $data The data payload.
690 *
691 * @return string
692 *
693 * @since 1.0.0
694 */
695 protected function make_multipart_body(array $data)
696 {
697 $data = $this->prepare_multipart_data($data);
698 $stream = new MultipartStream($data);
699 $boundary = $stream->boundary();
700 $this->content_type("multipart/form-data; boundary={$boundary}");
701 return $stream->payload();
702 }
703 /**
704 * Prepare the multipart data.
705 *
706 * @param array $data The data payload.
707 *
708 * @return array
709 *
710 * @since 1.0.0
711 */
712 protected function prepare_multipart_data(array $data)
713 {
714 return collection($data)->flat_map(function ($item, $key) {
715 if (\is_array($item)) {
716 if (isset($item['name']) && isset($item['contents'])) {
717 return $item;
718 }
719 return collection($item)->map(function ($value) use($key) {
720 return ['name' => $key . '[]', 'contents' => $value];
721 })->all();
722 }
723 return [['name' => $key, 'contents' => $item]];
724 })->values()->merge($this->files ?? [])->all();
725 }
726 /**
727 * Prepare the request arguments.
728 *
729 * @param mixed $data The data payload.
730 *
731 * @return array
732 *
733 * @since 1.0.0
734 */
735 protected function prepare_request_args($data)
736 {
737 return \array_merge($this->options, ['body' => $data ?: null]);
738 }
739 /**
740 * Check if the method is valid.
741 *
742 * @param string $method The method name.
743 *
744 * @return bool
745 *
746 * @since 1.0.0
747 */
748 protected function is_valid_method(string $method)
749 {
750 return \in_array(\strtoupper($method), $this->methods, \true);
751 }
752 /**
753 * Call a macro.
754 *
755 * @param mixed $method The method name.
756 * @param mixed $parameters The parameters array.
757 *
758 * @return mixed
759 *
760 * @throws \BadMethodCallException
761 *
762 * @since 1.0.0
763 */
764 public function __call($method, $parameters)
765 {
766 if (static::has_macro($method)) {
767 return $this->call_macro($method, $parameters);
768 }
769 throw new BadMethodCallException(\sprintf('Call to undefined method %s::%s', static::class, $method));
770 }
771 }
772