PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.14
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.14
6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 3.6.65 All 196 releases
fluentform / vendor / wpfluent / framework / src / WPFluent / Http / Client.php

Client.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder 6.2.14, at vendor/wpfluent/framework/src/WPFluent/Http/Client.php

662 lines 14.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentForm\Framework\Http;
4
5 use Exception;
6 use BadMethodCallException;
7 use FluentForm\Framework\Support\Arr;
8 use FluentForm\Framework\Support\Str;
9 use FluentForm\Framework\Foundation\App;
10 use FluentForm\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
49 class Client
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 \FluentForm\Framework\Http\Response
359 */
360 protected function request($url, $args = [])
361 {
362 return $this->dispatch(
363 $this->resolveUrl($url),
364 $this->filterArgs($args)
365 );
366 }
367
368 /**
369 * Build the URL.
370 *
371 * @param string $url
372 * @return string
373 */
374 protected function resolveUrl($url)
375 {
376 $q = $this->query;
377
378 $parsedUrl = parse_url($url);
379
380 $delimiter = isset($parsedUrl['query']) ? '&' : '?';
381
382 return $url . ($q ? $delimiter . http_build_query($q) : '');
383
384 }
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 \FluentForm\Framework\Http\Response
466 */
467 protected function makeResponse($response)
468 {
469 return new Response($response);
470 }
471
472 /**
473 * Handle the streaming response and process chunks.
474 *
475 * @param array $response
476 * @return \FluentForm\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);
497 }
498
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 }
513
514 /**
515 * Download a remote file.
516 *
517 * @param string $url
518 * @return \FluentForm\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 }
526
527 $parsed = parse_url($url);
528
529 if (!isset($parsed['scheme'])) {
530 $url = trim($this->baseUrl, '/') . '/' . trim($url, '/');
531 }
532
533 if (is_wp_error($file = download_url($url))) {
534 throw new Exception($file->get_error_message(), 500);
535 }
536
537 add_action('shutdown', function () use ($file) {
538 @unlink($file);
539 });
540
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 }
549
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 \FluentForm\Framework\Http\Response
558 */
559 public function uploadFile($url, $path, $fields = [], $name = 'file')
560 {
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);
607 }
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 */
657 public static function __callStatic($method, $args)
658 {
659 return static::make()->$method(...$args);
660 }
661 }
662