PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / trunk
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution vtrunk
2.4.0 2.3.0 2.2.5 2.2.0 2.1.2 2.1.1 trunk 1.10.0 1.10.01 1.10.02 1.5.0 1.5.01 1.5.02 1.5.1 1.5.10 1.5.20 1.5.21 1.5.22 1.5.23 1.5.24 1.5.25 1.6.0 1.7.0 1.7.1 1.7.2 All 33 releases
← All changes | vendor/wpfluent/framework/src/WPFluent/Http/Client.php +626 -72 1.5.21trunk View file →
@@ -1,107 +1,661 @@
1 1 <?php
2 2
3 3 namespace FluentBooking\Framework\Http;
4 4
5 +use Exception;
6 +use BadMethodCallException;
7 +use FluentBooking\Framework\Support\Arr;
8 +use FluentBooking\Framework\Support\Str;
9 +use FluentBooking\Framework\Foundation\App;
10 +use FluentBooking\Framework\Http\Request\File;
11 +
12 +/**
13 + * @method mixed get(string $url, array $params = []) Send a GET request.
14 + * @method mixed post(string $url, array $params = []) Send a POST request.
15 + * @method mixed put(string $url, array $params = []) Send a PUT request.
16 + * @method mixed patch(string $url, array $params = []) Send a PATCH request.
17 + * @method mixed delete(string $url, array $params = []) Send a DELETE request.
18 + * @method mixed head(string $url, array $params = []) Send a HEAD request.
19 + * @method mixed options(string $url, array $params = []) Send an OPTIONS request.
20 + * @method File download(string|File $url) Download a remote file.
21 + * @method mixed upload(string $url, string $path, array $fields = [], string $name = 'file') Upload a file to a remote server.
22 + */
23 +
24 +
25 +/**
26 + * Some examples:
27 + *
28 + * // Using an instance:
29 + * $client = Client::make('https://example.com');
30 + *
31 + * $response1 = $client->get('/users');
32 + * $response2 = $client->post('/users', ['body' => ['name' => 'Heera']]);
33 + * $response3 = $client->put('/users/1', ['body' => ['name' => 'Updated']]);
34 + * $response4 = $client->patch('/users/1', [
35 + * 'body' => ['email' => 'updated@example.com']]
36 + * );
37 + * $response5 = $client->delete('/users/1');
38 + * $response6 = $client->head('/users');
39 + * $response7 = $client->options('/users');
40 + *
41 + * // Using statically (default base URL is empty):
42 + * $response8 = Client::get('https://example.com/users');
43 + * $response9 = Client::post('https://example.com/users', [
44 + * 'body' => ['name' => 'Static']]
45 + * );
46 + */
47 +
48 +
5 49 class Client
6 50 {
51 + /**
52 + * Base URl for the request.
53 + *
54 + * @var string
55 + */
56 + protected $baseUrl = '';
57 +
58 + /**
59 + * Cookies to send with the request.
60 + *
61 + * @var array
62 + */
63 + protected $cookies = [];
64 +
65 + /**
66 + * Headers to send with the request.
67 + *
68 + * @var array
69 + */
70 + protected $headers = [];
71 +
72 + /**
73 + * Options to set for the request.
74 + *
75 + * @var array
76 + */
77 + protected $options = [];
78 +
79 + /**
80 + * Request body|Data|params to set in the request.
81 + *
82 + * @var array
83 + */
84 + protected $body = [];
85 +
86 + /**
87 + * Request query params to pass with the url.
88 + *
89 + * @var array
90 + */
91 + protected $query = [];
92 +
93 + /**
94 + * Whether to reject unsafe URLs via wp_safe_remote_request.
95 + * When true, dispatch() uses wp_safe_remote_request() instead of
96 + * wp_remote_request(), which runs the URL through wp_http_validate_url()
97 + * and refuses loopback addresses, private IP ranges, and non-standard
98 + * ports. Off by default for backwards compatibility with internal calls.
99 + *
100 + * @var bool
101 + */
102 + protected $useSafeRemote = false;
103 +
104 + /**
105 + * Stores args temporarily for then().
106 + *
107 + * @var null|array
108 + */
109 + private $args = null;
110 +
111 + /**
112 + * Create a new HTTP client.
113 + *
114 + * @param string $baseUrl
115 + * @param array $args
116 + */
117 + public function __construct($baseUrl = '', $args = [])
118 + {
119 + $this->baseUrl = rtrim($baseUrl, '/');
120 + $this->cookies = $args['cookies'] ?? [];
121 + $this->headers = $args['headers'] ?? [];
122 + $this->options = $args['options'] ?? [];
123 + }
124 +
125 + /**
126 + * Create a new HTTP client.
127 + *
128 + * @param string $baseUrl
129 + * @param array $args
130 + */
131 + public static function make($baseUrl = '', $args = [])
132 + {
133 + $args['cookies'] = $args['cookies'] ?? [];
134 + $args['headers'] = $args['headers'] ?? [];
135 + $args['options'] = $args['options'] ?? [];
136 + return new static($baseUrl, $args);
137 + }
138 +
139 + /**
140 + * Sets one or more options.
141 + *
142 + * @return self
143 + */
144 + public function withOption($key, $value = null)
145 + {
146 + $options = is_array($key) ? $key : [$key => $value];
147 +
148 + foreach ($options as $key => $value) {
149 + $this->options[$key] = $value;
150 + }
151 +
152 + return $this;
153 + }
154 +
155 + /**
156 + * Sets the blocking option to false (non-blocking).
157 + *
158 + * @return self
159 + */
160 + public function async()
161 + {
162 + return $this->withOption('blocking', false);
163 + }
164 +
165 + /**
166 + * Sets the sslverify option.
167 + *
168 + * @return self
169 + */
170 + public function secure($verify = true)
171 + {
172 + return $this->withOption('sslverify', $verify);
173 + }
174 +
175 + /**
176 + * Route dispatch through wp_safe_remote_request() instead of
177 + * wp_remote_request() — mirrors WordPress's wp_safe_remote_* family.
178 + *
179 + * Runs the URL through wp_http_validate_url() first, which refuses
180 + * loopback addresses, private IP ranges, and non-standard ports. Use
181 + * when the target URL is user/admin-configurable and you need SSRF
182 + * protection. Standard external HTTPS endpoints on ports 80/443/8080
183 + * are unaffected.
184 + *
185 + * @param bool $enabled
186 + * @return self
187 + */
188 + public function safe($enabled = true)
189 + {
190 + $this->useSafeRemote = $enabled;
191 + return $this;
192 + }
193 +
194 + /**
195 + * Sets one or more headers.
196 + *
197 + * @return self
198 + */
199 + public function withHeader($key, $value = null)
200 + {
201 + $headers = is_array($key) ? $key : [$key => $value];
202 +
203 + foreach ($headers as $key => $value) {
204 + $this->headers[$key] = $value;
205 + }
206 +
207 + return $this;
208 + }
209 +
210 + /**
211 + * Sets one or more headers.
212 + *
213 + * @param array $headers
214 + * @return self
215 + */
216 + public function withHeaders(array $headers)
217 + {
218 + return $this->withHeader($headers);
219 + }
220 +
221 + /**
222 + * Sets the Authorization header.
223 + *
224 + * @param string $token
225 + * @param string $type
226 + * @return self
227 + */
228 + public function withToken($token, $type = 'Bearer')
229 + {
230 + return $this->withHeader([
231 + 'Authorization' => $type . ' ' . $token,
232 + ]);
233 + }
234 +
235 + /**
236 + * Sets one or more cookies.
237 + *
238 + * @return self
239 + */
240 + public function withCookie($key, $value = null)
241 + {
242 + $cookies = is_array($key) ? $key : [$key => $value];
243 +
244 + foreach ($cookies as $key => $value) {
245 + $this->cookies[$key] = $value;
246 + }
247 +
248 + return $this;
249 + }
250 +
251 + /**
252 + * Sets one or more request body param.
253 + *
254 + * @return self
255 + */
256 + public function withData($key, $value = null)
257 + {
258 + $data = is_array($key) ? $key : [$key => $value];
259 +
260 + foreach ($data as $key => $value) {
261 + $this->body[$key] = $value;
262 + }
263 +
264 + return $this;
265 + }
266 +
267 + /**
268 + * Sets one or more request body param.
269 + *
270 + * @return self
271 + */
272 + public function withBody($key, $value = null)
273 + {
274 + return $this->withData($key, $value);
275 + }
276 +
277 + /**
278 + * Sets one or more request body param.
279 + *
280 + * @return self
281 + */
282 + public function withParam($key, $value = null)
283 + {
284 + return $this->withData($key, $value);
285 + }
286 +
287 + /**
288 + * Sets one or more request body param.
289 + *
290 + * @return self
291 + */
292 + public function withQuery($key, $value = null)
293 + {
294 + $data = is_array($key) ? $key : [$key => $value];
295 +
296 + foreach ($data as $key => $value) {
297 + $this->query[$key] = $value;
298 + }
299 +
300 + return $this;
301 + }
302 +
303 + /**
304 + * Allows users to enable streaming on their requests.
305 + *
306 + * @return self
307 + */
308 + public function withStreaming($callback = null)
309 + {
310 + return $this->withOption(
311 + 'stream', true
312 + )->withOption('stream_callback', $callback);
313 + }
314 +
315 + /**
316 + * Build the request arguments.
317 + *
318 + * @param array $params
319 + * @param string $method
320 + * @return array
321 + */
322 + protected function buildRequestArgs($params, $method)
323 + {
324 + $defaultParams = [
325 + 'body' => [],
326 + 'cookies' => [],
327 + 'headers' => [],
328 + ];
329 +
330 + $params = wp_parse_args($params[0] ?? [], $defaultParams);
331 +
332 + $options = array_merge($this->options, $params['options'] ?? []);
333 +
334 + if (Str::isJson($params['body'])) {
335 + $this->withHeader('Content-Type', 'application/json');
336 + $params['body'] = json_decode($params['body'], true);
337 + }
338 +
339 + $params = [
340 + 'method' => strtoupper($method),
341 + 'body' => array_merge($this->body, $params['body']),
342 + 'cookies' => array_merge($this->cookies, $params['cookies']),
343 + 'headers' => array_merge($this->headers, $params['headers']),
344 + ];
345 +
346 + foreach($options as $key => $value) {
347 + $params[$key] = $value;
348 + }
349 +
350 + return $params;
351 + }
352 +
353 + /**
354 + * Send the request.
355 + *
356 + * @param string $url
357 + * @param array $args
358 + * @return \FluentBooking\Framework\Http\Response
359 + */
7 360 protected function request($url, $args = [])
8 361 {
9 - $response = wp_remote_request($url, $args);
362 + return $this->dispatch(
363 + $this->resolveUrl($url),
364 + $this->filterArgs($args)
365 + );
366 + }
10 367
11 - if (is_wp_error($response)) {
12 - throw new class(
13 - $response->get_error_message(), 500
14 - ) extends \Exception {};
15 - }
368 + /**
369 + * Build the URL.
370 + *
371 + * @param string $url
372 + * @return string
373 + */
374 + protected function resolveUrl($url)
375 + {
376 + $q = $this->query;
16 377
17 - return $this->makeResponse($response);
378 + $parsedUrl = parse_url($url);
379 +
380 + $delimiter = isset($parsedUrl['query']) ? '&' : '?';
381 +
382 + return $url . ($q ? $delimiter . http_build_query($q) : '');
383 +
18 384 }
19 385
386 + /**
387 + * Filter the args before sending the request.
388 + *
389 + * @param array $args
390 + * @return array
391 + */
392 + protected function filterArgs($args)
393 + {
394 + if ($this->shouldBeJson($args)) {
395 + $args['body'] = json_encode($args['body']);
396 + }
397 +
398 + return $args;
399 + }
400 +
401 + /**
402 + * Encode the body if Content-Type is JSON.
403 + *
404 + * @param array $params
405 + * @return bool
406 + */
407 + protected function shouldBeJson($params)
408 + {
409 + return isset(
410 + $params['headers']['Content-Type']
411 + ) && $params['headers']['Content-Type'] === 'application/json';
412 + }
413 +
414 + /**
415 + * Dispatch the request.
416 + *
417 + * @param string $url
418 + * @param array $args
419 + * @return array (response)
420 + */
421 + protected function dispatch($url, $args)
422 + {
423 + $fn = $this->useSafeRemote ? 'wp_safe_remote_request' : 'wp_remote_request';
424 + $response = $fn($url, $args);
425 +
426 + if (is_wp_error($response)) {
427 + throw new Exception($response->get_error_message(), 500);
428 + }
429 +
430 + $this->mergeCookies($response);
431 +
432 + if ($this->isStreamEnabled($args)) {
433 + return $this->makeStreamResponse($response);
434 + }
435 +
436 + return $this->makeResponse($response);
437 + }
438 +
439 + /**
440 + * Merge the coolkies (useful for stateful request).
441 + *
442 + * @return void
443 + */
444 + protected function mergeCookies($response)
445 + {
446 + $this->cookies = array_merge(
447 + $this->cookies,
448 + wp_remote_retrieve_cookies($response)
449 + );
450 + }
451 +
452 + /**
453 + * Check if the stream is enabled.
454 + * @return boolean
455 + */
456 + protected function isStreamEnabled($args)
457 + {
458 + return isset($args['stream']) && $args['stream'];
459 + }
460 +
461 + /**
462 + * Build a response object from an anonymous class.
463 + *
464 + * @param array $response
465 + * @return \FluentBooking\Framework\Http\Response
466 + */
20 467 protected function makeResponse($response)
21 468 {
22 - return new class($response) {
469 + return new Response($response);
470 + }
23 471
24 - protected $response = null;
25 -
26 - public function __construct($response) {
27 - $this->response = $response;
472 + /**
473 + * Handle the streaming response and process chunks.
474 + *
475 + * @param array $response
476 + * @return \FluentBooking\Framework\Http\Response|null
477 + */
478 + protected function makeStreamResponse($response)
479 + {
480 + $response['body'] = $response['filename'];
481 +
482 + return new class($response) extends Response implements \ArrayAccess {
483 + public function flush() {
484 + $source = fopen($this->response['body'], 'rb');
485 +
486 + $fp = fopen('php://output', 'wb');
487 +
488 + while (!feof($source)) {
489 + $data = fread($source, 8192);
490 + fwrite($fp, $data);
491 + ob_flush();
492 + flush();
493 + }
494 +
495 + fclose($source);
496 + fclose($fp);
28 497 }
29 498
30 - public function toArray() {
31 - return $this->response;
32 - }
499 + #[ReturnTypeWillChange]
500 + public function offsetGet($offset) {
501 + return $this->response[$offset] ?? null;
502 + }
503 + #[ReturnTypeWillChange]
504 + public function offsetSet($offset, $value) {
505 + $this->response[$offset] = $value;
506 + }
507 + #[ReturnTypeWillChange]
508 + public function offsetExists($offset) {}
509 + #[ReturnTypeWillChange]
510 + public function offsetUnset($offset) {}
511 + };
512 + }
33 513
34 - public function isOkay() {
35 - return $this->getCode() == 200;
36 - }
514 + /**
515 + * Download a remote file.
516 + *
517 + * @param string $url
518 + * @return \FluentBooking\Framework\Http\Request\File
519 + * @throws \Exception
520 + */
521 + public function downloadFile($url)
522 + {
523 + if (!function_exists('download_url')) {
524 + require_once ABSPATH . 'wp-admin/includes/file.php';
525 + }
37 526
38 - public function throw() {
39 - $class = sprintf(
40 - 'WpOrg\Requests\Exception\Http\Status%d', $this->getCode()
41 - );
42 -
43 - if (!class_exists($class)) {
44 - $class = 'WpOrg\Requests\Exception\Http\Status\Http';
45 - }
46 -
47 - throw new $class;
48 - }
527 + $parsed = parse_url($url);
49 528
50 - public function throwIf(callable $callback) {
51 - if ($callback($this)) {
52 - return $this->throw();
53 - }
54 - }
529 + if (!isset($parsed['scheme'])) {
530 + $url = trim($this->baseUrl, '/') . '/' . trim($url, '/');
531 + }
55 532
56 - public function getCode() {
57 - return wp_remote_retrieve_response_code($this->response);
58 - }
533 + if (is_wp_error($file = download_url($url))) {
534 + throw new Exception($file->get_error_message(), 500);
535 + }
59 536
60 - public function getMessage() {
61 - return wp_remote_retrieve_response_message($this->response);
62 - }
537 + add_action('shutdown', function () use ($file) {
538 + @unlink($file);
539 + });
63 540
64 - public function getBody() {
65 - return wp_remote_retrieve_body($this->response);
66 - }
541 + return new File(
542 + $file,
543 + basename($url),
544 + mime_content_type($file) ?: 'application/octet-stream',
545 + filesize($file),
546 + UPLOAD_ERR_OK
547 + );
548 + }
67 549
68 - public function isJson() {
69 - $header = $this->getHeader('content-type');
70 - return str_contains($header, 'application/json');
71 - }
550 + /**
551 + * Upload a file to a remote server.
552 + *
553 + * @param string $url
554 + * @param string $path
555 + * @param array $fields
556 + * @param string $name
557 + * @return \FluentBooking\Framework\Http\Response
558 + */
559 + public function uploadFile($url, $path, $fields = [], $name = 'file')
560 + {
561 + $path = $path instanceof File ? $path->getPathname() : $path;
72 562
73 - public function getJson() {
74 - if ($this->isJson()) {
75 - return json_decode($this->getBody(), true);
76 - }
77 - }
563 + if (!file_exists($path)) {
564 + throw new Exception('File does not exist.', 500);
565 + }
78 566
79 - public function getHeaders() {
80 - return wp_remote_retrieve_headers($this->response);
81 - }
567 + // Auto prepend base URL if $url is relative
568 + if (strpos($url, 'http') !== 0) {
569 + $url = rtrim($this->baseUrl, '/') . '/' . ltrim($url, '/');
570 + }
82 571
83 - public function getHeader($key) {
84 - return wp_remote_retrieve_header($this->response, $key);
85 - }
572 + $boundary = wp_generate_password(24, false);
86 573
87 - public function getCookies() {
88 - return wp_remote_retrieve_cookies($this->response);
89 - }
574 + $headers = array_merge(
575 + $this->headers,
576 + [
577 + 'Accept' => '*/*',
578 + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
579 + ]
580 + );
90 581
91 - public function getCookie($name, $isObject = false) {
92 - if ($isObject) {
93 - // Return the WP_Http_Cookie object
94 - return wp_remote_retrieve_cookie($this->response, $name);
95 - }
96 - return wp_remote_retrieve_cookie_value($this->response, $name);
97 - }
98 - };
582 + $fileName = basename($path);
583 + $content = file_get_contents($path);
584 + $mime = mime_content_type($path);
585 +
586 + $body = '';
587 +
588 + foreach ($fields as $key => $value) {
589 + $body .= "--" . $boundary . "\r\n";
590 + $body .= 'Content-Disposition: form-data; name="' . $key . '"' . "\r\n\r\n";
591 + $body .= $value . "\r\n";
592 + }
593 +
594 + $body .= "--" . $boundary . "\r\n";
595 + $body .= 'Content-Disposition: form-data; name="'.$name.'"; filename="' . $fileName . '"' . "\r\n";
596 + $body .= 'Content-Type: ' . $mime . "\r\n\r\n";
597 + $body .= $content . "\r\n";
598 + $body .= "--" . $boundary . "--\r\n";
599 +
600 + $response = wp_remote_post($url, [
601 + 'headers' => $headers,
602 + 'body' => $body,
603 + 'timeout' => 60,
604 + ]);
605 +
606 + return $this->makeResponse($response);
99 607 }
100 608
609 + protected function checkIfValidHttpMethod($method)
610 + {
611 + $validHttpMethods = [
612 + 'get', 'post', 'put', 'delete', 'patch', 'options', 'head'
613 + ];
614 +
615 + if (!in_array(strtolower($method), $validHttpMethods)) {
616 + throw new BadMethodCallException("Method $method does not exist.");
617 + }
618 + }
619 +
620 + /**
621 + * Handles the dynamic calls.
622 + *
623 + * @param string $method
624 + * @param array $args
625 + * @return mixed
626 + */
627 + public function __call($method, $args)
628 + {
629 + if ($method === 'download') {
630 + return $this->downloadFile(...$args);
631 + }
632 +
633 + // Handles dynamic method calls like:
634 + // get, post and so on.
635 + $url = array_shift($args);
636 +
637 + $parsed = parse_url($url);
638 +
639 + if (!isset($parsed['scheme'])) {
640 + $url = trim($this->baseUrl, '/') . '/' . trim($url, '/');
641 + }
642 +
643 + $this->checkIfValidHttpMethod($method);
644 +
645 + return $this->request(
646 + $url, $this->buildRequestArgs($args, $method)
647 + );
648 + }
649 +
650 + /**
651 + * Handle the static dynamic calls.
652 + *
653 + * @param string $method
654 + * @param array $args
655 + * @return self
656 + */
101 657 public static function __callStatic($method, $args)
102 658 {
103 - return (new static)->request(array_shift($args), array_merge(
104 - count($args) ? $args : [], ['method' => strtoupper($method)]
105 - ));
659 + return static::make()->$method(...$args);
106 660 }
107 661 }