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 +594 -71 1.312.1.0 View file →
@@ -1,107 +1,630 @@
1 1 <?php
2 2
3 3 namespace FluentBoards\Framework\Http;
4 4
5 +use Exception;
6 +use BadMethodCallException;
7 +use FluentBoards\Framework\Support\Arr;
8 +use FluentBoards\Framework\Support\Str;
9 +use FluentBoards\Framework\Foundation\App;
10 +use FluentBoards\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 + * Stores args temporarily for then().
95 + *
96 + * @var null|array
97 + */
98 + private $args = null;
99 +
100 + /**
101 + * Create a new HTTP client.
102 + *
103 + * @param string $baseUrl
104 + * @param array $args
105 + */
106 + public function __construct($baseUrl = '', $args = [])
107 + {
108 + $this->baseUrl = rtrim($baseUrl, '/');
109 + $this->cookies = $args['cookies'] ?? [];
110 + $this->headers = $args['headers'] ?? [];
111 + $this->options = $args['options'] ?? [];
112 + }
113 +
114 + /**
115 + * Create a new HTTP client.
116 + *
117 + * @param string $baseUrl
118 + * @param array $args
119 + */
120 + public static function make($baseUrl = '', $args = [])
121 + {
122 + $args['cookies'] = $args['cookies'] ?? [];
123 + $args['headers'] = $args['headers'] ?? [];
124 + $args['options'] = $args['options'] ?? [];
125 + return new static($baseUrl, $args);
126 + }
127 +
128 + /**
129 + * Sets one or more options.
130 + *
131 + * @return self
132 + */
133 + public function withOption($key, $value = null)
134 + {
135 + $options = is_array($key) ? $key : [$key => $value];
136 +
137 + foreach ($options as $key => $value) {
138 + $this->options[$key] = $value;
139 + }
140 +
141 + return $this;
142 + }
143 +
144 + /**
145 + * Sets the blocking option to false (non-blocking).
146 + *
147 + * @return self
148 + */
149 + public function async()
150 + {
151 + return $this->withOption('blocking', false);
152 + }
153 +
154 + /**
155 + * Sets the sslverify option.
156 + *
157 + * @return self
158 + */
159 + public function secure($verify = true)
160 + {
161 + return $this->withOption('sslverify', $verify);
162 + }
163 +
164 + /**
165 + * Sets one or more headers.
166 + *
167 + * @return self
168 + */
169 + public function withHeader($key, $value = null)
170 + {
171 + $headers = is_array($key) ? $key : [$key => $value];
172 +
173 + foreach ($headers as $key => $value) {
174 + $this->headers[$key] = $value;
175 + }
176 +
177 + return $this;
178 + }
179 +
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 + /**
206 + * Sets one or more cookies.
207 + *
208 + * @return self
209 + */
210 + public function withCookie($key, $value = null)
211 + {
212 + $cookies = is_array($key) ? $key : [$key => $value];
213 +
214 + foreach ($cookies as $key => $value) {
215 + $this->cookies[$key] = $value;
216 + }
217 +
218 + return $this;
219 + }
220 +
221 + /**
222 + * Sets one or more request body param.
223 + *
224 + * @return self
225 + */
226 + public function withData($key, $value = null)
227 + {
228 + $data = is_array($key) ? $key : [$key => $value];
229 +
230 + foreach ($data as $key => $value) {
231 + $this->body[$key] = $value;
232 + }
233 +
234 + return $this;
235 + }
236 +
237 + /**
238 + * Sets one or more request body param.
239 + *
240 + * @return self
241 + */
242 + public function withBody($key, $value = null)
243 + {
244 + return $this->withData($key, $value);
245 + }
246 +
247 + /**
248 + * Sets one or more request body param.
249 + *
250 + * @return self
251 + */
252 + public function withParam($key, $value = null)
253 + {
254 + return $this->withData($key, $value);
255 + }
256 +
257 + /**
258 + * Sets one or more request body param.
259 + *
260 + * @return self
261 + */
262 + public function withQuery($key, $value = null)
263 + {
264 + $data = is_array($key) ? $key : [$key => $value];
265 +
266 + foreach ($data as $key => $value) {
267 + $this->query[$key] = $value;
268 + }
269 +
270 + return $this;
271 + }
272 +
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 + /**
286 + * Build the request arguments.
287 + *
288 + * @param array $params
289 + * @param string $method
290 + * @return array
291 + */
292 + protected function buildRequestArgs($params, $method)
293 + {
294 + $defaultParams = [
295 + 'body' => [],
296 + 'cookies' => [],
297 + 'headers' => [],
298 + ];
299 +
300 + $params = wp_parse_args($params[0] ?? [], $defaultParams);
301 +
302 + $options = array_merge($this->options, $params['options'] ?? []);
303 +
304 + if (Str::isJson($params['body'])) {
305 + $this->withHeader('Content-Type', 'application/json');
306 + $params['body'] = json_decode($params['body'], true);
307 + }
308 +
309 + $params = [
310 + 'method' => strtoupper($method),
311 + 'body' => array_merge($this->body, $params['body']),
312 + 'cookies' => array_merge($this->cookies, $params['cookies']),
313 + 'headers' => array_merge($this->headers, $params['headers']),
314 + ];
315 +
316 + foreach($options as $key => $value) {
317 + $params[$key] = $value;
318 + }
319 +
320 + return $params;
321 + }
322 +
323 + /**
324 + * Send the request.
325 + *
326 + * @param string $url
327 + * @param array $args
328 + * @return \FluentBoards\Framework\Http\Response
329 + */
7 330 protected function request($url, $args = [])
8 331 {
332 + return $this->dispatch(
333 + $this->resolveUrl($url),
334 + $this->filterArgs($args)
335 + );
336 + }
337 +
338 + /**
339 + * Build the URL.
340 + *
341 + * @param string $url
342 + * @return string
343 + */
344 + protected function resolveUrl($url)
345 + {
346 + $q = $this->query;
347 +
348 + $parsedUrl = parse_url($url);
349 +
350 + $delimiter = isset($parsedUrl['query']) ? '&' : '?';
351 +
352 + return $url . ($q ? $delimiter . http_build_query($q) : '');
353 +
354 + }
355 +
356 + /**
357 + * Filter the args before sending the request.
358 + *
359 + * @param array $args
360 + * @return array
361 + */
362 + protected function filterArgs($args)
363 + {
364 + if ($this->shouldBeJson($args)) {
365 + $args['body'] = json_encode($args['body']);
366 + }
367 +
368 + return $args;
369 + }
370 +
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';
382 + }
383 +
384 + /**
385 + * Dispatch the request.
386 + *
387 + * @param string $url
388 + * @param array $args
389 + * @return array (response)
390 + */
391 + protected function dispatch($url, $args)
392 + {
9 393 $response = wp_remote_request($url, $args);
10 394
11 - if (is_wp_error($response)) {
12 - throw new class(
13 - $response->get_error_message(), 500
14 - ) extends \Exception {};
15 - }
395 + if (is_wp_error($response)) {
396 + throw new Exception($response->get_error_message(), 500);
397 + }
16 398
17 - return $this->makeResponse($response);
399 + $this->mergeCookies($response);
400 +
401 + if ($this->isStreamEnabled($args)) {
402 + return $this->makeStreamResponse($response);
403 + }
404 +
405 + return $this->makeResponse($response);
18 406 }
19 407
408 + /**
409 + * Merge the coolkies (useful for stateful request).
410 + *
411 + * @return void
412 + */
413 + protected function mergeCookies($response)
414 + {
415 + $this->cookies = array_merge(
416 + $this->cookies,
417 + wp_remote_retrieve_cookies($response)
418 + );
419 + }
420 +
421 + /**
422 + * Check if the stream is enabled.
423 + * @return boolean
424 + */
425 + protected function isStreamEnabled($args)
426 + {
427 + return isset($args['stream']) && $args['stream'];
428 + }
429 +
430 + /**
431 + * Build a response object from an anonymous class.
432 + *
433 + * @param array $response
434 + * @return \FluentBoards\Framework\Http\Response
435 + */
20 436 protected function makeResponse($response)
21 437 {
22 - return new class($response) {
438 + return new Response($response);
439 + }
23 440
24 - protected $response = null;
25 -
26 - public function __construct($response) {
27 - $this->response = $response;
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'];
450 +
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');
456 +
457 + while (!feof($source)) {
458 + $data = fread($source, 8192);
459 + fwrite($fp, $data);
460 + ob_flush();
461 + flush();
462 + }
463 +
464 + fclose($source);
465 + fclose($fp);
28 466 }
29 467
30 - public function toArray() {
31 - return $this->response;
32 - }
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 + };
481 + }
33 482
34 - public function isOkay() {
35 - return $this->getCode() == 200;
36 - }
483 + /**
484 + * Download a remote file.
485 + *
486 + * @param string $url
487 + * @return \FluentBoards\Framework\Http\Request\File
488 + * @throws \Exception
489 + */
490 + public function downloadFile($url)
491 + {
492 + if (!function_exists('download_url')) {
493 + require_once ABSPATH . 'wp-admin/includes/file.php';
494 + }
37 495
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 - }
496 + $parsed = parse_url($url);
49 497
50 - public function throwIf(callable $callback) {
51 - if ($callback($this)) {
52 - return $this->throw();
53 - }
54 - }
498 + if (!isset($parsed['scheme'])) {
499 + $url = trim($this->baseUrl, '/') . '/' . trim($url, '/');
500 + }
55 501
56 - public function getCode() {
57 - return wp_remote_retrieve_response_code($this->response);
58 - }
502 + if (is_wp_error($file = download_url($url))) {
503 + throw new Exception($file->get_error_message(), 500);
504 + }
59 505
60 - public function getMessage() {
61 - return wp_remote_retrieve_response_message($this->response);
62 - }
506 + add_action('shutdown', function () use ($file) {
507 + @unlink($file);
508 + });
63 509
64 - public function getBody() {
65 - return wp_remote_retrieve_body($this->response);
66 - }
510 + return new File(
511 + $file,
512 + basename($url),
513 + mime_content_type($file) ?: 'application/octet-stream',
514 + filesize($file),
515 + UPLOAD_ERR_OK
516 + );
517 + }
67 518
68 - public function isJson() {
69 - $header = $this->getHeader('content-type');
70 - return str_contains($header, 'application/json');
71 - }
519 + /**
520 + * Upload a file to a remote server.
521 + *
522 + * @param string $url
523 + * @param string $path
524 + * @param array $fields
525 + * @param string $name
526 + * @return \FluentBoards\Framework\Http\Response
527 + */
528 + public function uploadFile($url, $path, $fields = [], $name = 'file')
529 + {
530 + $path = $path instanceof File ? $path->getPathname() : $path;
72 531
73 - public function getJson() {
74 - if ($this->isJson()) {
75 - return json_decode($this->getBody(), true);
76 - }
77 - }
532 + if (!file_exists($path)) {
533 + throw new Exception('File does not exist.', 500);
534 + }
78 535
79 - public function getHeaders() {
80 - return wp_remote_retrieve_headers($this->response);
81 - }
536 + // Auto prepend base URL if $url is relative
537 + if (strpos($url, 'http') !== 0) {
538 + $url = rtrim($this->baseUrl, '/') . '/' . ltrim($url, '/');
539 + }
82 540
83 - public function getHeader($key) {
84 - return wp_remote_retrieve_header($this->response, $key);
85 - }
541 + $boundary = wp_generate_password(24, false);
86 542
87 - public function getCookies() {
88 - return wp_remote_retrieve_cookies($this->response);
89 - }
543 + $headers = array_merge(
544 + $this->headers,
545 + [
546 + 'Accept' => '*/*',
547 + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
548 + ]
549 + );
90 550
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 - };
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);
99 576 }
100 577
578 + protected function checkIfValidHttpMethod($method)
579 + {
580 + $validHttpMethods = [
581 + 'get', 'post', 'put', 'delete', 'patch', 'options', 'head'
582 + ];
583 +
584 + if (!in_array(strtolower($method), $validHttpMethods)) {
585 + throw new BadMethodCallException("Method $method does not exist.");
586 + }
587 + }
588 +
589 + /**
590 + * Handles the dynamic calls.
591 + *
592 + * @param string $method
593 + * @param array $args
594 + * @return mixed
595 + */
596 + public function __call($method, $args)
597 + {
598 + if ($method === 'download') {
599 + return $this->downloadFile(...$args);
600 + }
601 +
602 + // Handles dynamic method calls like:
603 + // get, post and so on.
604 + $url = array_shift($args);
605 +
606 + $parsed = parse_url($url);
607 +
608 + if (!isset($parsed['scheme'])) {
609 + $url = trim($this->baseUrl, '/') . '/' . trim($url, '/');
610 + }
611 +
612 + $this->checkIfValidHttpMethod($method);
613 +
614 + return $this->request(
615 + $url, $this->buildRequestArgs($args, $method)
616 + );
617 + }
618 +
619 + /**
620 + * Handle the static dynamic calls.
621 + *
622 + * @param string $method
623 + * @param array $args
624 + * @return self
625 + */
101 626 public static function __callStatic($method, $args)
102 627 {
103 - return (new static)->request(array_shift($args), array_merge(
104 - count($args) ? $args : [], ['method' => strtoupper($method)]
105 - ));
628 + return static::make()->$method(...$args);
106 629 }
107 630 }