PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.1.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.1.0
2.1.0 2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 1.41 All 42 releases
← All changes | vendor/wpfluent/framework/src/WPFluent/Http/Client.php +232 -165 1.402.1.0 View file →
@@ -4,22 +4,49 @@
4 4
5 5 use Exception;
6 6 use BadMethodCallException;
7 7 use FluentBoards\Framework\Support\Arr;
8 +use FluentBoards\Framework\Support\Str;
8 9 use FluentBoards\Framework\Foundation\App;
9 10 use FluentBoards\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.
@@ -77,9 +104,9 @@
77 104 * @param array $args
78 105 */
79 106 public function __construct($baseUrl = '', $args = [])
80 107 {
81 - $this->baseUrl = $baseUrl;
108 + $this->baseUrl = rtrim($baseUrl, '/');
82 109 $this->cookies = $args['cookies'] ?? [];
83 110 $this->headers = $args['headers'] ?? [];
84 111 $this->options = $args['options'] ?? [];
85 112 }
@@ -150,8 +177,33 @@
150 177 return $this;
151 178 }
152 179
153 180 /**
181 + * Sets one or more headers.
182 + *
183 + * @param array $headers
184 + * @return self
185 + */
186 + public function withHeaders(array $headers)
187 + {
188 + return $this->withHeader($headers);
189 + }
190 +
191 + /**
192 + * Sets the Authorization header.
193 + *
194 + * @param string $token
195 + * @param string $type
196 + * @return self
197 + */
198 + public function withToken($token, $type = 'Bearer')
199 + {
200 + return $this->withHeader([
201 + 'Authorization' => $type . ' ' . $token,
202 + ]);
203 + }
204 +
205 + /**
154 206 * Sets one or more cookies.
155 207 *
156 208 * @return self
157 209 */
@@ -218,8 +270,20 @@
218 270 return $this;
219 271 }
220 272
221 273 /**
274 + * Allows users to enable streaming on their requests.
275 + *
276 + * @return self
277 + */
278 + public function withStreaming($callback = null)
279 + {
280 + return $this->withOption(
281 + 'stream', true
282 + )->withOption('stream_callback', $callback);
283 + }
284 +
285 + /**
222 286 * Build the request arguments.
223 287 *
224 288 * @param array $params
225 289 * @param string $method
@@ -232,14 +296,15 @@
232 296 'cookies' => [],
233 297 'headers' => [],
234 298 ];
235 299
236 - $callback = isset($params[1]) ? $params[1] : null;
300 + $params = wp_parse_args($params[0] ?? [], $defaultParams);
237 301
238 - $params = wp_parse_args(reset($params), $defaultParams);
302 + $options = array_merge($this->options, $params['options'] ?? []);
239 303
240 - if ($callback) {
241 - $params['callback'] = $callback;
304 + if (Str::isJson($params['body'])) {
305 + $this->withHeader('Content-Type', 'application/json');
306 + $params['body'] = json_decode($params['body'], true);
242 307 }
243 308
244 309 $params = [
245 310 'method' => strtoupper($method),
@@ -245,13 +310,10 @@
245 310 'method' => strtoupper($method),
246 311 'body' => array_merge($this->body, $params['body']),
247 312 'cookies' => array_merge($this->cookies, $params['cookies']),
248 313 'headers' => array_merge($this->headers, $params['headers']),
249 - 'callback' => $params['callback'] ?? null,
250 314 ];
251 315
252 - $options = array_merge($this->options, $params['options'] ?? []);
253 -
254 316 foreach($options as $key => $value) {
255 317 $params[$key] = $value;
256 318 }
257 319
@@ -262,199 +324,164 @@
262 324 * Send the request.
263 325 *
264 326 * @param string $url
265 327 * @param array $args
266 - * @return Response object from anonymous class.
328 + * @return \FluentBoards\Framework\Http\Response
267 329 */
268 330 protected function request($url, $args = [])
269 331 {
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)
332 + return $this->dispatch(
333 + $this->resolveUrl($url),
334 + $this->filterArgs($args)
283 335 );
284 -
285 - return $this->makeResponse($response);
286 336 }
287 337
288 338 /**
289 - * Send the request.
339 + * Build the URL.
290 340 *
291 341 * @param string $url
292 - * @param array $args
293 - * @return Response object from anonymous class.
342 + * @return string
294 343 */
295 - protected function asyncRequest($url, $args = [])
344 + protected function resolveUrl($url)
296 345 {
297 - $args['url'] = $url;
346 + $q = $this->query;
298 347
299 - if ($query = http_build_query($this->query)) {
300 - $args['url'] .= '?' . $query;
301 - }
348 + $parsedUrl = parse_url($url);
302 349
303 - $this->args = $args;
350 + $delimiter = isset($parsedUrl['query']) ? '&' : '?';
304 351
305 - if (isset($args['callback'])) {
306 - $this->then($args['callback']);
307 - }
352 + return $url . ($q ? $delimiter . http_build_query($q) : '');
308 353
309 - return $this;
310 354 }
311 355
312 356 /**
313 - * Add the callback for handling the response.
357 + * Filter the args before sending the request.
314 358 *
315 - * @param callable $callback
316 - * @return void
359 + * @param array $args
360 + * @return array
317 361 */
318 - public function then($callback)
362 + protected function filterArgs($args)
319 363 {
320 - if ($callback instanceof \Closure) {
321 - throw new Exception(
322 - 'The callback must not be a closure', 500
323 - );
324 - }
364 + if ($this->shouldBeJson($args)) {
365 + $args['body'] = json_encode($args['body']);
366 + }
325 367
326 - if (is_string($callback) && function_exists($callback)) {
327 - throw new Exception(
328 - 'The callback must not be a function', 500
329 - );
330 - }
368 + return $args;
369 + }
331 370
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 - }
371 + /**
372 + * Encode the body if Content-Type is JSON.
373 + *
374 + * @param array $params
375 + * @return bool
376 + */
377 + protected function shouldBeJson($params)
378 + {
379 + return isset(
380 + $params['headers']['Content-Type']
381 + ) && $params['headers']['Content-Type'] === 'application/json';
354 382 }
355 383
356 384 /**
357 - * Register the shutdown handler.
385 + * Dispatch the request.
358 386 *
387 + * @param string $url
359 388 * @param array $args
360 - * @return void
389 + * @return array (response)
361 390 */
362 - protected function registerShutdownHandler($args)
391 + protected function dispatch($url, $args)
363 392 {
364 - $this->serializeCallback($args);
393 + $response = wp_remote_request($url, $args);
365 394
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 - });
395 + if (is_wp_error($response)) {
396 + throw new Exception($response->get_error_message(), 500);
397 + }
398 +
399 + $this->mergeCookies($response);
400 +
401 + if ($this->isStreamEnabled($args)) {
402 + return $this->makeStreamResponse($response);
403 + }
404 +
405 + return $this->makeResponse($response);
378 406 }
379 407
380 408 /**
381 - * Serializes the callback.
409 + * Merge the coolkies (useful for stateful request).
382 410 *
383 - * @param array &$args
384 411 * @return void
385 412 */
386 - protected function serializeCallback(&$args)
413 + protected function mergeCookies($response)
387 414 {
388 - $args['callback'] = base64_encode(serialize($args['callback']));
415 + $this->cookies = array_merge(
416 + $this->cookies,
417 + wp_remote_retrieve_cookies($response)
418 + );
389 419 }
390 420
391 421 /**
392 - * Get the closure.
393 - *
394 - * @param Array &$params
395 - * @return \Closure
422 + * Check if the stream is enabled.
423 + * @return boolean
396 424 */
397 - protected static function getCallback(&$params)
425 + protected function isStreamEnabled($args)
398 426 {
399 - $callback = unserialize(base64_decode($params['callback']));
400 -
401 - unset($params['callback']);
402 -
403 - return $callback;
427 + return isset($args['stream']) && $args['stream'];
404 428 }
405 429
406 430 /**
407 - * Register the main async request handler.
431 + * Build a response object from an anonymous class.
408 432 *
409 - * @return void
433 + * @param array $response
434 + * @return \FluentBoards\Framework\Http\Response
410 435 */
411 - public static function registerAsyncRequestHandler()
436 + protected function makeResponse($response)
412 437 {
413 - $action = static::makeAsyncRequestAction();
438 + return new Response($response);
439 + }
414 440
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'];
441 + /**
442 + * Handle the streaming response and process chunks.
443 + *
444 + * @param array $response
445 + * @return \FluentBoards\Framework\Http\Response|null
446 + */
447 + protected function makeStreamResponse($response)
448 + {
449 + $response['body'] = $response['filename'];
428 450
429 -
430 - $callback = static::getCallback($params);
451 + return new class($response) extends Response implements \ArrayAccess {
452 + public function flush() {
453 + $source = fopen($this->response['body'], 'rb');
454 +
455 + $fp = fopen('php://output', 'wb');
431 456
432 - $response = $client->{$requestMethod}('', $params);
457 + while (!feof($source)) {
458 + $data = fread($source, 8192);
459 + fwrite($fp, $data);
460 + ob_flush();
461 + flush();
462 + }
433 463
434 - if (is_wp_error($response)) {
435 - $exception = new Exception(
436 - $response->get_error_message(), 500
437 - );
464 + fclose($source);
465 + fclose($fp);
438 466 }
439 467
440 - return $callback($response, $exception ?? null);
441 - });
468 + #[ReturnTypeWillChange]
469 + public function offsetGet($offset) {
470 + return $this->response[$offset] ?? null;
471 + }
472 + #[ReturnTypeWillChange]
473 + public function offsetSet($offset, $value) {
474 + $this->response[$offset] = $value;
475 + }
476 + #[ReturnTypeWillChange]
477 + public function offsetExists($offset) {}
478 + #[ReturnTypeWillChange]
479 + public function offsetUnset($offset) {}
480 + };
442 481 }
443 482
444 483 /**
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 484 * Download a remote file.
458 485 *
459 486 * @param string $url
460 487 * @return \FluentBoards\Framework\Http\Request\File
@@ -489,16 +516,64 @@
489 516 );
490 517 }
491 518
492 519 /**
493 - * Build a response object from an anonymous class.
520 + * Upload a file to a remote server.
494 521 *
495 - * @param array $response
496 - * @return @return Response object from anonymous class.
522 + * @param string $url
523 + * @param string $path
524 + * @param array $fields
525 + * @param string $name
526 + * @return \FluentBoards\Framework\Http\Response
497 527 */
498 - protected function makeResponse($response)
528 + public function uploadFile($url, $path, $fields = [], $name = 'file')
499 529 {
500 - return new Response($response);
530 + $path = $path instanceof File ? $path->getPathname() : $path;
531 +
532 + if (!file_exists($path)) {
533 + throw new Exception('File does not exist.', 500);
534 + }
535 +
536 + // Auto prepend base URL if $url is relative
537 + if (strpos($url, 'http') !== 0) {
538 + $url = rtrim($this->baseUrl, '/') . '/' . ltrim($url, '/');
539 + }
540 +
541 + $boundary = wp_generate_password(24, false);
542 +
543 + $headers = array_merge(
544 + $this->headers,
545 + [
546 + 'Accept' => '*/*',
547 + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
548 + ]
549 + );
550 +
551 + $fileName = basename($path);
552 + $content = file_get_contents($path);
553 + $mime = mime_content_type($path);
554 +
555 + $body = '';
556 +
557 + foreach ($fields as $key => $value) {
558 + $body .= "--" . $boundary . "\r\n";
559 + $body .= 'Content-Disposition: form-data; name="' . $key . '"' . "\r\n\r\n";
560 + $body .= $value . "\r\n";
561 + }
562 +
563 + $body .= "--" . $boundary . "\r\n";
564 + $body .= 'Content-Disposition: form-data; name="'.$name.'"; filename="' . $fileName . '"' . "\r\n";
565 + $body .= 'Content-Type: ' . $mime . "\r\n\r\n";
566 + $body .= $content . "\r\n";
567 + $body .= "--" . $boundary . "--\r\n";
568 +
569 + $response = wp_remote_post($url, [
570 + 'headers' => $headers,
571 + 'body' => $body,
572 + 'timeout' => 60,
573 + ]);
574 +
575 + return $this->makeResponse($response);
501 576 }
502 577
503 578 protected function checkIfValidHttpMethod($method)
504 579 {
@@ -524,10 +599,9 @@
524 599 return $this->downloadFile(...$args);
525 600 }
526 601
527 602 // Handles dynamic method calls like:
528 - // asyncGet, asyncPost and so on
529 - // get, post and so on
603 + // get, post and so on.
530 604 $url = array_shift($args);
531 605
532 606 $parsed = parse_url($url);
533 607
@@ -533,18 +607,11 @@
533 607
534 608 if (!isset($parsed['scheme'])) {
535 609 $url = trim($this->baseUrl, '/') . '/' . trim($url, '/');
536 610 }
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 611
546 612 $this->checkIfValidHttpMethod($method);
613 +
547 614 return $this->request(
548 615 $url, $this->buildRequestArgs($args, $method)
549 616 );
550 617 }