PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.5.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.5.0
2.5.0 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 All 34 releases
← All changes | vendor/wpfluent/framework/src/WPFluent/Http/Client.php +230 -199 1.7.1 → 2.5.0 View file →
@@ -4,23 +4,49 @@
4 4
5 5 use Exception;
6 6 use BadMethodCallException;
7 7 use FluentBooking\Framework\Support\Arr;
8 +use FluentBooking\Framework\Support\Str;
8 9 use FluentBooking\Framework\Foundation\App;
9 10 use FluentBooking\Framework\Http\Request\File;
10 11
11 12 /**
12 - * @method mixed get(string $url, $params = []) Send a GET request.
13 - * @method mixed post(string $url, $params = []) Send a POST request.
14 - * @method mixed put(string $url, $params = []) Send a PUT request.
15 - * @method mixed delete(string $url, $params = []) Send a DELETE request.
16 - * @method Client asynGet(string $url, $params = []) Send an async GET request.
17 - * @method Client asyncPost(string $url, $params = []) Send an async POST request.
18 - * @method Client asyncPut(string $url, $params = []) Send an async PUT request.
19 - * @method Client asyncDelete(string $url, $params = []) Send an async DELETE request.
20 - * @method File download(string|File $url) Download a remote file.
21 - * @method mixed upload(string $url, string $path, string $name = 'file') upload a file to a remote server.
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 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' => '[email protected]']]
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 +
23 49 class Client
24 50 {
25 51 /**
26 52 * Base URl for the request.
@@ -58,16 +84,27 @@
58 84 protected $body = [];
59 85
60 86 /**
61 87 * Request query params to pass with the url.
62 - *
88 + *
63 89 * @var array
64 90 */
65 91 protected $query = [];
66 92
67 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 + /**
68 105 * Stores args temporarily for then().
69 - *
106 + *
70 107 * @var null|array
71 108 */
72 109 private $args = null;
73 110
@@ -78,9 +115,9 @@
78 115 * @param array $args
79 116 */
80 117 public function __construct($baseUrl = '', $args = [])
81 118 {
82 - $this->baseUrl = $baseUrl;
119 + $this->baseUrl = rtrim($baseUrl, '/');
83 120 $this->cookies = $args['cookies'] ?? [];
84 121 $this->headers = $args['headers'] ?? [];
85 122 $this->options = $args['options'] ?? [];
86 123 }
@@ -126,9 +163,9 @@
126 163 }
127 164
128 165 /**
129 166 * Sets the sslverify option.
130 - *
167 + *
131 168 * @return self
132 169 */
133 170 public function secure($verify = true)
134 171 {
@@ -135,8 +172,27 @@
135 172 return $this->withOption('sslverify', $verify);
136 173 }
137 174
138 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 + /**
139 195 * Sets one or more headers.
140 196 *
141 197 * @return self
142 198 */
@@ -151,8 +207,33 @@
151 207 return $this;
152 208 }
153 209
154 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 + /**
155 236 * Sets one or more cookies.
156 237 *
157 238 * @return self
158 239 */
@@ -219,8 +300,20 @@
219 300 return $this;
220 301 }
221 302
222 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 + /**
223 316 * Build the request arguments.
224 317 *
225 318 * @param array $params
226 319 * @param string $method
@@ -233,16 +326,15 @@
233 326 'cookies' => [],
234 327 'headers' => [],
235 328 ];
236 329
237 - $callback = isset($params[1]) ? $params[1] : null;
330 + $params = wp_parse_args($params[0] ?? [], $defaultParams);
238 331
239 - $params = wp_parse_args(reset($params), $defaultParams);
240 -
241 332 $options = array_merge($this->options, $params['options'] ?? []);
242 333
243 - if ($callback) {
244 - $params['callback'] = $callback;
334 + if (Str::isJson($params['body'])) {
335 + $this->withHeader('Content-Type', 'application/json');
336 + $params['body'] = json_decode($params['body'], true);
245 337 }
246 338
247 339 $params = [
248 340 'method' => strtoupper($method),
@@ -248,12 +340,10 @@
248 340 'method' => strtoupper($method),
249 341 'body' => array_merge($this->body, $params['body']),
250 342 'cookies' => array_merge($this->cookies, $params['cookies']),
251 343 'headers' => array_merge($this->headers, $params['headers']),
252 - 'callback' => $params['callback'] ?? null,
253 344 ];
254 345
255 -
256 346 foreach($options as $key => $value) {
257 347 $params[$key] = $value;
258 348 }
259 349
@@ -268,206 +358,158 @@
268 358 * @return \FluentBooking\Framework\Http\Response
269 359 */
270 360 protected function request($url, $args = [])
271 361 {
272 - if ($query = http_build_query($this->query)) {
273 - $url .= '?' . $query;
274 - }
275 -
276 - $response = wp_remote_request($url, $args);
277 -
278 - if (is_wp_error($response)) {
279 - throw new Exception($response->get_error_message(), 500);
280 - }
281 -
282 - $this->cookies = array_merge(
283 - $this->cookies,
284 - wp_remote_retrieve_cookies($response)
362 + return $this->dispatch(
363 + $this->resolveUrl($url),
364 + $this->filterArgs($args)
285 365 );
286 -
287 - return $this->makeResponse($response);
288 366 }
289 367
290 368 /**
291 - * Send the request.
369 + * Build the URL.
292 370 *
293 371 * @param string $url
294 - * @param array $args
295 - * @return \FluentBooking\Framework\Http\Response
372 + * @return string
296 373 */
297 - protected function asyncRequest($url, $args = [])
374 + protected function resolveUrl($url)
298 375 {
299 - $args['url'] = $url;
376 + $q = $this->query;
300 377
301 - if ($query = http_build_query($this->query)) {
302 - $args['url'] .= '?' . $query;
303 - }
378 + $parsedUrl = parse_url($url);
304 379
305 - $this->args = $args;
380 + $delimiter = isset($parsedUrl['query']) ? '&' : '?';
306 381
307 - if (isset($args['callback'])) {
308 - $this->then($args['callback']);
309 - }
382 + return $url . ($q ? $delimiter . http_build_query($q) : '');
310 383
311 - return $this;
312 384 }
313 385
314 386 /**
315 - * Add the callback for handling the response.
387 + * Filter the args before sending the request.
316 388 *
317 - * @param callable $callback
318 - * @return void
389 + * @param array $args
390 + * @return array
319 391 */
320 - // public function then($callback)
321 - // {
322 - // // Normalize [ClassName::class, 'method'] to 'ClassName@method'
323 - // if (
324 - // is_array($callback) &&
325 - // count($callback) === 2 &&
326 - // is_string($callback[0]) &&
327 - // is_string($callback[1])
328 - // ) {
329 - // $callback = implode('@', $callback);
330 - // }
392 + protected function filterArgs($args)
393 + {
394 + if ($this->shouldBeJson($args)) {
395 + $args['body'] = json_encode($args['body']);
396 + }
331 397
332 - // if (is_string($callback) && function_exists($callback)) {
333 - // throw new Exception(
334 - // 'The callback must not be a function', 500
335 - // );
336 - // }
398 + return $args;
399 + }
337 400
338 - // if (!is_string($callback) || !str_contains($callback, '@')) {
339 - // throw new Exception(
340 - // 'The callback must be a string in the format Class@method', 500
341 - // );
342 - // }
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 + }
343 413
344 - // $this->args['callback'] = $callback;
345 -
346 - // $this->registerShutdownHandler($this->args);
347 - // }
348 -
349 414 /**
350 - * Register the shutdown handler.
415 + * Dispatch the request.
351 416 *
417 + * @param string $url
352 418 * @param array $args
353 - * @return void
419 + * @return array (response)
354 420 */
355 - // protected function registerShutdownHandler($args)
356 - // {
357 - // $this->serializeCallback($args);
421 + protected function dispatch($url, $args)
422 + {
423 + $fn = $this->useSafeRemote ? 'wp_safe_remote_request' : 'wp_remote_request';
424 + $response = $fn($url, $args);
358 425
359 - // add_action('shutdown', function() use ($args) {
360 - // $action = static::makeAsyncRequestAction();
361 - // wp_remote_post(admin_url('admin-post.php'), [
362 - // 'timeout' => 1,
363 - // 'blocking' => false,
364 - // 'sslverify' => false,
365 - // 'body' => [
366 - // 'args' => $args,
367 - // 'action' => $action
368 - // ],
369 - // ]);
370 - // });
371 - // }
426 + if (is_wp_error($response)) {
427 + throw new Exception($response->get_error_message(), 500);
428 + }
372 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 +
373 439 /**
374 - * Serializes the callback.
440 + * Merge the coolkies (useful for stateful request).
375 441 *
376 - * @param array &$args
377 442 * @return void
378 443 */
379 - protected function serializeCallback(&$args)
444 + protected function mergeCookies($response)
380 445 {
381 - $args['callback'] = base64_encode(json_encode($args['callback']));
446 + $this->cookies = array_merge(
447 + $this->cookies,
448 + wp_remote_retrieve_cookies($response)
449 + );
382 450 }
383 451
384 452 /**
385 - * Get the closure.
386 - *
387 - * @param Array &$params
388 - * @return \Closure
453 + * Check if the stream is enabled.
454 + * @return boolean
389 455 */
390 - protected static function getCallback(&$params)
456 + protected function isStreamEnabled($args)
391 457 {
392 - $callback = json_decode(
393 - base64_decode($params['callback']), true
394 - );
395 -
396 - if (!is_string($callback) || !str_contains($callback, '@')) {
397 - throw new Exception('Invalid callback.');
398 - }
399 -
400 - unset($params['callback']);
401 -
402 - [$class, $method] = explode('@', $callback, 2);
403 -
404 - if (!class_exists($class)) {
405 - throw new Exception("Class {$class} not found.");
406 - }
407 -
408 - $instance = App::make($class);
409 -
410 - if (
411 - !method_exists($instance, $method) ||
412 - !is_callable([$instance, $method])
413 - ) {
414 - throw new Exception(
415 - "Method {$method} not callable on {$class}."
416 - );
417 - }
418 -
419 - return [$instance, $method];
458 + return isset($args['stream']) && $args['stream'];
420 459 }
421 460
422 461 /**
423 - * Register the main async request handler.
462 + * Build a response object from an anonymous class.
424 463 *
425 - * @return void
464 + * @param array $response
465 + * @return \FluentBooking\Framework\Http\Response
426 466 */
427 - // public static function registerAsyncRequestHandler()
428 - // {
429 - // $action = static::makeAsyncRequestAction();
467 + protected function makeResponse($response)
468 + {
469 + return new Response($response);
470 + }
430 471
431 - // App::addAction("admin_post_nopriv_{$action}", function() {
432 -
433 - // $request = App::make('request');
434 -
435 - // $requestUrl = $request->get('args.url');
436 -
437 - // $requestMethod = $request->get('args.method');
438 -
439 - // $client = Client::make($requestUrl);
440 -
441 - // $params = $request->except(
442 - // 'action', 'args.url', 'args.method',
443 - // )['args'];
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'];
444 481
445 -
446 - // $callback = static::getCallback($params);
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');
447 487
448 - // $response = $client->{$requestMethod}('', $params);
488 + while (!feof($source)) {
489 + $data = fread($source, 8192);
490 + fwrite($fp, $data);
491 + ob_flush();
492 + flush();
493 + }
449 494
450 - // if (is_wp_error($response)) {
451 - // $exception = new Exception(
452 - // $response->get_error_message(), 500
453 - // );
454 - // }
495 + fclose($source);
496 + fclose($fp);
497 + }
455 498
456 - // return $callback($response, $exception ?? null);
457 - // });
458 - // }
459 -
460 - /**
461 - * Make the action for async request.
462 - *
463 - * @return string
464 - */
465 - protected static function makeAsyncRequestAction()
466 - {
467 - return 'wpf-async-request-'.sha1(
468 - App::config()->get('app.slug')
469 - );
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 + };
470 512 }
471 513
472 514 /**
473 515 * Download a remote file.
@@ -511,9 +553,9 @@
511 553 * @param string $url
512 554 * @param string $path
513 555 * @param array $fields
514 556 * @param string $name
515 - * @return \WpAgent\Http\Response
557 + * @return \FluentBooking\Framework\Http\Response
516 558 */
517 559 public function uploadFile($url, $path, $fields = [], $name = 'file')
518 560 {
519 561 $path = $path instanceof File ? $path->getPathname() : $path;
@@ -521,14 +563,22 @@
521 563 if (!file_exists($path)) {
522 564 throw new Exception('File does not exist.', 500);
523 565 }
524 566
567 + // Auto prepend base URL if $url is relative
568 + if (strpos($url, 'http') !== 0) {
569 + $url = rtrim($this->baseUrl, '/') . '/' . ltrim($url, '/');
570 + }
571 +
525 572 $boundary = wp_generate_password(24, false);
526 573
527 - $headers = [
528 - 'Accept' => '*/*',
529 - 'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
530 - ];
574 + $headers = array_merge(
575 + $this->headers,
576 + [
577 + 'Accept' => '*/*',
578 + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
579 + ]
580 + );
531 581
532 582 $fileName = basename($path);
533 583 $content = file_get_contents($path);
534 584 $mime = mime_content_type($path);
@@ -555,19 +605,8 @@
555 605
556 606 return $this->makeResponse($response);
557 607 }
558 608
559 - /**
560 - * Build a response object from an anonymous class.
561 - *
562 - * @param array $response
563 - * @return @return \FluentBooking\Framework\Http\Response
564 - */
565 - protected function makeResponse($response)
566 - {
567 - return new Response($response);
568 - }
569 -
570 609 protected function checkIfValidHttpMethod($method)
571 610 {
572 611 $validHttpMethods = [
573 612 'get', 'post', 'put', 'delete', 'patch', 'options', 'head'
@@ -591,10 +630,9 @@
591 630 return $this->downloadFile(...$args);
592 631 }
593 632
594 633 // Handles dynamic method calls like:
595 - // asyncGet, asyncPost and so on
596 - // get, post and so on
634 + // get, post and so on.
597 635 $url = array_shift($args);
598 636
599 637 $parsed = parse_url($url);
600 638
@@ -600,18 +638,11 @@
600 638
601 639 if (!isset($parsed['scheme'])) {
602 640 $url = trim($this->baseUrl, '/') . '/' . trim($url, '/');
603 641 }
604 -
605 - if (str_starts_with($method, 'async')) {
606 - $method = substr($method, strlen('async'));
607 - $this->checkIfValidHttpMethod($method);
608 - return $this->asyncRequest(
609 - $url, $this->buildRequestArgs($args, $method)
610 - );
611 - }
612 642
613 643 $this->checkIfValidHttpMethod($method);
644 +
614 645 return $this->request(
615 646 $url, $this->buildRequestArgs($args, $method)
616 647 );
617 648 }