PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.11.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.11.0
2.11.0 2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 All 78 releases
← All changes | vendor/wpfluent/framework/src/WPFluent/Http/Client.php +266 -168 1.0.902.11.0 View file →
@@ -4,22 +4,49 @@
4 4
5 5 use Exception;
6 6 use BadMethodCallException;
7 7 use FluentCommunity\Framework\Support\Arr;
8 +use FluentCommunity\Framework\Support\Str;
8 9 use FluentCommunity\Framework\Foundation\App;
9 10 use FluentCommunity\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.
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.
21 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 +
22 49 class Client
23 50 {
24 51 /**
25 52 * Base URl for the request.
@@ -57,16 +84,27 @@
57 84 protected $body = [];
58 85
59 86 /**
60 87 * Request query params to pass with the url.
61 - *
88 + *
62 89 * @var array
63 90 */
64 91 protected $query = [];
65 92
66 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 + /**
67 105 * Stores args temporarily for then().
68 - *
106 + *
69 107 * @var null|array
70 108 */
71 109 private $args = null;
72 110
@@ -77,9 +115,9 @@
77 115 * @param array $args
78 116 */
79 117 public function __construct($baseUrl = '', $args = [])
80 118 {
81 - $this->baseUrl = $baseUrl;
119 + $this->baseUrl = rtrim($baseUrl, '/');
82 120 $this->cookies = $args['cookies'] ?? [];
83 121 $this->headers = $args['headers'] ?? [];
84 122 $this->options = $args['options'] ?? [];
85 123 }
@@ -125,9 +163,9 @@
125 163 }
126 164
127 165 /**
128 166 * Sets the sslverify option.
129 - *
167 + *
130 168 * @return self
131 169 */
132 170 public function secure($verify = true)
133 171 {
@@ -134,8 +172,27 @@
134 172 return $this->withOption('sslverify', $verify);
135 173 }
136 174
137 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 + /**
138 195 * Sets one or more headers.
139 196 *
140 197 * @return self
141 198 */
@@ -150,8 +207,33 @@
150 207 return $this;
151 208 }
152 209
153 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 + /**
154 236 * Sets one or more cookies.
155 237 *
156 238 * @return self
157 239 */
@@ -218,8 +300,20 @@
218 300 return $this;
219 301 }
220 302
221 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 + /**
222 316 * Build the request arguments.
223 317 *
224 318 * @param array $params
225 319 * @param string $method
@@ -232,14 +326,15 @@
232 326 'cookies' => [],
233 327 'headers' => [],
234 328 ];
235 329
236 - $callback = isset($params[1]) ? $params[1] : null;
330 + $params = wp_parse_args($params[0] ?? [], $defaultParams);
237 331
238 - $params = wp_parse_args(reset($params), $defaultParams);
332 + $options = array_merge($this->options, $params['options'] ?? []);
239 333
240 - if ($callback) {
241 - $params['callback'] = $callback;
334 + if (Str::isJson($params['body'])) {
335 + $this->withHeader('Content-Type', 'application/json');
336 + $params['body'] = json_decode($params['body'], true);
242 337 }
243 338
244 339 $params = [
245 340 'method' => strtoupper($method),
@@ -245,13 +340,10 @@
245 340 'method' => strtoupper($method),
246 341 'body' => array_merge($this->body, $params['body']),
247 342 'cookies' => array_merge($this->cookies, $params['cookies']),
248 343 'headers' => array_merge($this->headers, $params['headers']),
249 - 'callback' => $params['callback'] ?? null,
250 344 ];
251 345
252 - $options = array_merge($this->options, $params['options'] ?? []);
253 -
254 346 foreach($options as $key => $value) {
255 347 $params[$key] = $value;
256 348 }
257 349
@@ -262,199 +354,165 @@
262 354 * Send the request.
263 355 *
264 356 * @param string $url
265 357 * @param array $args
266 - * @return Response object from anonymous class.
358 + * @return \FluentCommunity\Framework\Http\Response
267 359 */
268 360 protected function request($url, $args = [])
269 361 {
270 - if ($query = http_build_query($this->query)) {
271 - $url .= '?' . $query;
272 - }
273 -
274 - $response = wp_remote_request($url, $args);
275 -
276 - if (is_wp_error($response)) {
277 - throw new Exception($response->get_error_message(), 500);
278 - }
279 -
280 - $this->cookies = array_merge(
281 - $this->cookies,
282 - wp_remote_retrieve_cookies($response)
362 + return $this->dispatch(
363 + $this->resolveUrl($url),
364 + $this->filterArgs($args)
283 365 );
284 -
285 - return $this->makeResponse($response);
286 366 }
287 367
288 368 /**
289 - * Send the request.
369 + * Build the URL.
290 370 *
291 371 * @param string $url
292 - * @param array $args
293 - * @return Response object from anonymous class.
372 + * @return string
294 373 */
295 - protected function asyncRequest($url, $args = [])
374 + protected function resolveUrl($url)
296 375 {
297 - $args['url'] = $url;
376 + $q = $this->query;
298 377
299 - if ($query = http_build_query($this->query)) {
300 - $args['url'] .= '?' . $query;
301 - }
378 + $parsedUrl = parse_url($url);
302 379
303 - $this->args = $args;
380 + $delimiter = isset($parsedUrl['query']) ? '&' : '?';
304 381
305 - if (isset($args['callback'])) {
306 - $this->then($args['callback']);
307 - }
382 + return $url . ($q ? $delimiter . http_build_query($q) : '');
308 383
309 - return $this;
310 384 }
311 385
312 386 /**
313 - * Add the callback for handling the response.
387 + * Filter the args before sending the request.
314 388 *
315 - * @param callable $callback
316 - * @return void
389 + * @param array $args
390 + * @return array
317 391 */
318 - public function then($callback)
392 + protected function filterArgs($args)
319 393 {
320 - if ($callback instanceof \Closure) {
321 - throw new Exception(
322 - 'The callback must not be a closure', 500
323 - );
324 - }
394 + if ($this->shouldBeJson($args)) {
395 + $args['body'] = json_encode($args['body']);
396 + }
325 397
326 - if (is_string($callback) && function_exists($callback)) {
327 - throw new Exception(
328 - 'The callback must not be a function', 500
329 - );
330 - }
398 + return $args;
399 + }
331 400
332 - // Normalize [Example::class, method] to 'Example@method'
333 - if (is_array($callback) && is_string(reset($callback))) {
334 - $callback = implode('@', $callback);
335 - }
336 -
337 - if (is_string($callback)) {
338 - if (str_contains($callback, '@')) {
339 - [$class, $method] = explode('@', $callback);
340 - $callback = [App::make($class), $method];
341 - } elseif (method_exists($callback, '__invoke')) {
342 - $callback = App::make($callback);
343 - }
344 - }
345 -
346 - if (!is_callable($callback)) {
347 - throw new Exception('Callback must be callable', 500);
348 - }
349 -
350 - if (is_callable($callback)) {
351 - $this->args['callback'] = $callback;
352 - $this->registerShutdownHandler($this->args);
353 - }
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';
354 412 }
355 413
356 414 /**
357 - * Register the shutdown handler.
415 + * Dispatch the request.
358 416 *
417 + * @param string $url
359 418 * @param array $args
360 - * @return void
419 + * @return array (response)
361 420 */
362 - protected function registerShutdownHandler($args)
421 + protected function dispatch($url, $args)
363 422 {
364 - $this->serializeCallback($args);
423 + $fn = $this->useSafeRemote ? 'wp_safe_remote_request' : 'wp_remote_request';
424 + $response = $fn($url, $args);
365 425
366 - add_action('shutdown', function() use ($args) {
367 - $action = static::makeAsyncRequestAction();
368 - wp_remote_post(admin_url('admin-post.php'), [
369 - 'timeout' => 1,
370 - 'blocking' => false,
371 - 'sslverify' => false,
372 - 'body' => [
373 - 'args' => $args,
374 - 'action' => $action
375 - ],
376 - ]);
377 - });
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);
378 437 }
379 438
380 439 /**
381 - * Serializes the callback.
440 + * Merge the coolkies (useful for stateful request).
382 441 *
383 - * @param array &$args
384 442 * @return void
385 443 */
386 - protected function serializeCallback(&$args)
444 + protected function mergeCookies($response)
387 445 {
388 - $args['callback'] = base64_encode(serialize($args['callback']));
446 + $this->cookies = array_merge(
447 + $this->cookies,
448 + wp_remote_retrieve_cookies($response)
449 + );
389 450 }
390 451
391 452 /**
392 - * Get the closure.
393 - *
394 - * @param Array &$params
395 - * @return \Closure
453 + * Check if the stream is enabled.
454 + * @return boolean
396 455 */
397 - protected static function getCallback(&$params)
456 + protected function isStreamEnabled($args)
398 457 {
399 - $callback = unserialize(base64_decode($params['callback']));
400 -
401 - unset($params['callback']);
402 -
403 - return $callback;
458 + return isset($args['stream']) && $args['stream'];
404 459 }
405 460
406 461 /**
407 - * Register the main async request handler.
462 + * Build a response object from an anonymous class.
408 463 *
409 - * @return void
464 + * @param array $response
465 + * @return \FluentCommunity\Framework\Http\Response
410 466 */
411 - public static function registerAsyncRequestHandler()
467 + protected function makeResponse($response)
412 468 {
413 - $action = static::makeAsyncRequestAction();
469 + return new Response($response);
470 + }
414 471
415 - App::addAction("admin_post_nopriv_{$action}", function() {
416 -
417 - $request = App::make('request');
418 -
419 - $requestUrl = $request->get('args.url');
420 -
421 - $requestMethod = $request->get('args.method');
422 -
423 - $client = Client::make($requestUrl);
424 -
425 - $params = $request->except(
426 - 'action', 'args.url', 'args.method',
427 - )['args'];
472 + /**
473 + * Handle the streaming response and process chunks.
474 + *
475 + * @param array $response
476 + * @return \FluentCommunity\Framework\Http\Response|null
477 + */
478 + protected function makeStreamResponse($response)
479 + {
480 + $response['body'] = $response['filename'];
428 481
429 -
430 - $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');
431 487
432 - $response = $client->{$requestMethod}('', $params);
488 + while (!feof($source)) {
489 + $data = fread($source, 8192);
490 + fwrite($fp, $data);
491 + ob_flush();
492 + flush();
493 + }
433 494
434 - if (is_wp_error($response)) {
435 - $exception = new Exception(
436 - $response->get_error_message(), 500
437 - );
495 + fclose($source);
496 + fclose($fp);
438 497 }
439 498
440 - return $callback($response, $exception ?? null);
441 - });
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 + };
442 512 }
443 513
444 514 /**
445 - * Make the action for async request.
446 - *
447 - * @return string
448 - */
449 - protected static function makeAsyncRequestAction()
450 - {
451 - return 'wpf-async-request-'.sha1(
452 - App::config()->get('app.slug')
453 - );
454 - }
455 -
456 - /**
457 515 * Download a remote file.
458 516 *
459 517 * @param string $url
460 518 * @return \FluentCommunity\Framework\Http\Request\File
@@ -489,16 +547,64 @@
489 547 );
490 548 }
491 549
492 550 /**
493 - * Build a response object from an anonymous class.
551 + * Upload a file to a remote server.
494 552 *
495 - * @param array $response
496 - * @return @return Response object from anonymous class.
553 + * @param string $url
554 + * @param string $path
555 + * @param array $fields
556 + * @param string $name
557 + * @return \FluentCommunity\Framework\Http\Response
497 558 */
498 - protected function makeResponse($response)
559 + public function uploadFile($url, $path, $fields = [], $name = 'file')
499 560 {
500 - return new Response($response);
561 + $path = $path instanceof File ? $path->getPathname() : $path;
562 +
563 + if (!file_exists($path)) {
564 + throw new Exception('File does not exist.', 500);
565 + }
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 +
572 + $boundary = wp_generate_password(24, false);
573 +
574 + $headers = array_merge(
575 + $this->headers,
576 + [
577 + 'Accept' => '*/*',
578 + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
579 + ]
580 + );
581 +
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);
501 607 }
502 608
503 609 protected function checkIfValidHttpMethod($method)
504 610 {
@@ -524,10 +630,9 @@
524 630 return $this->downloadFile(...$args);
525 631 }
526 632
527 633 // Handles dynamic method calls like:
528 - // asyncGet, asyncPost and so on
529 - // get, post and so on
634 + // get, post and so on.
530 635 $url = array_shift($args);
531 636
532 637 $parsed = parse_url($url);
533 638
@@ -533,18 +638,11 @@
533 638
534 639 if (!isset($parsed['scheme'])) {
535 640 $url = trim($this->baseUrl, '/') . '/' . trim($url, '/');
536 641 }
537 -
538 - if (str_starts_with($method, 'async')) {
539 - $method = substr($method, strlen('async'));
540 - $this->checkIfValidHttpMethod($method);
541 - return $this->asyncRequest(
542 - $url, $this->buildRequestArgs($args, $method)
543 - );
544 - }
545 642
546 643 $this->checkIfValidHttpMethod($method);
644 +
547 645 return $this->request(
548 646 $url, $this->buildRequestArgs($args, $method)
549 647 );
550 648 }