Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.38% covered (success)
90.38%
94 / 104
77.78% covered (success)
77.78%
14 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
Curl
90.38% covered (success)
90.38%
94 / 104
77.78% covered (success)
77.78%
14 / 18
60.99
0.00% covered (danger)
0.00%
0 / 1
 __construct
87.50% covered (success)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 create
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setOption
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setMethod
66.67% covered (warning)
66.67%
10 / 15
0.00% covered (danger)
0.00%
0 / 1
12.00
 setReturnTransfer
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setReturnHeader
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setVerifyPeer
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 allowSelfSigned
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isReturnTransfer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 isReturnHeader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 isVerifyPeer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 isAllowSelfSigned
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getInfo
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 prepare
90.62% covered (success)
90.62%
29 / 32
0.00% covered (danger)
0.00%
0 / 1
17.24
 send
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 parseResponse
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
6.01
 reset
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 disconnect
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Pop PHP Framework (http://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Http\Client\Handler;
15
16use Pop\Http\Auth;
17use Pop\Http\Parser;
18use Pop\Http\Client\Request;
19use Pop\Http\Client\Response;
20use Pop\Mime\Message;
21
22/**
23 * HTTP client curl handler class
24 *
25 * @category   Pop
26 * @package    Pop\Http
27 * @author     Nick Sagona, III <dev@nolainteractive.com>
28 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
29 * @license    http://www.popphp.org/license     New BSD License
30 * @version    5.0.0
31 */
32class Curl extends AbstractCurl
33{
34
35    /**
36     * Constructor
37     *
38     * Instantiate the Curl handler object
39     *
40     * @param  ?array $options
41     * @param  bool   $default
42     * @throws Exception
43     */
44    public function __construct(?array $options = null, bool $default = true)
45    {
46        if (!function_exists('curl_init')) {
47            throw new Exception('Error: Curl is not available.');
48        }
49
50        $this->resource = curl_init();
51
52        if ($default) {
53            $this->setOption(CURLOPT_HEADER, true);
54            $this->setOption(CURLOPT_RETURNTRANSFER, true);
55        }
56
57        if (!empty($options)) {
58            $this->setOptions($options);
59        }
60    }
61
62    /**
63     * Factory method to create a Curl handler
64     *
65     * @param  string $method
66     * @param  ?array $options
67     * @param  bool  $default
68     * @return Curl
69     *
70     *@throws Exception
71     */
72    public static function create(string $method = 'GET', ?array $options = null, bool $default = true): Curl
73    {
74        $handler = new self($options, $default);
75        $handler->setMethod($method);
76        return $handler;
77    }
78
79    /**
80     * Set Curl option
81     *
82     * @param  int   $opt
83     * @param  mixed $val
84     * @return AbstractCurl
85     */
86    public function setOption(int $opt, mixed $val): AbstractCurl
87    {
88        parent::setOption($opt, $val);
89        curl_setopt($this->resource, $opt, $val);
90
91        return $this;
92    }
93
94    /**
95     * Set the method
96     *
97     * @param  string $method
98     * @param  bool   $forceCustom
99     * @return Curl
100     */
101    public function setMethod(string $method, bool $forceCustom = false): Curl
102    {
103        if ($method == 'GET') {
104            if ($this->hasOption(CURLOPT_POST)) {
105                $this->removeOption(CURLOPT_POST);
106            }
107            if ($this->hasOption(CURLOPT_CUSTOMREQUEST)) {
108                $this->removeOption(CURLOPT_CUSTOMREQUEST);
109            }
110        } else if (($method == 'POST') && (!$forceCustom)) {
111            $this->setOption(CURLOPT_POST, true);
112            if ($this->hasOption(CURLOPT_CUSTOMREQUEST)) {
113                $this->removeOption(CURLOPT_CUSTOMREQUEST);
114            }
115        } else {
116            $this->setOption(CURLOPT_CUSTOMREQUEST, $method);
117        }
118
119        if ($method == 'HEAD') {
120            $this->setOption(CURLOPT_NOBODY, true);
121        } else if ($this->hasOption(CURLOPT_NOBODY)) {
122            $this->removeOption(CURLOPT_NOBODY);
123        }
124
125        return $this;
126    }
127
128    /**
129     * Set Curl option to return the transfer (set to true by default)
130     *
131     * @param  bool $transfer
132     * @return Curl
133     */
134    public function setReturnTransfer(bool $transfer = true): Curl
135    {
136        $this->setOption(CURLOPT_RETURNTRANSFER, (bool)$transfer);
137        return $this;
138    }
139
140    /**
141     * Set Curl option to return the headers (set to true by default)
142     *
143     * @param  bool $header
144     * @return Curl
145     */
146    public function setReturnHeader(bool $header = true): Curl
147    {
148        $this->setOption(CURLOPT_HEADER, (bool)$header);
149        return $this;
150    }
151
152    /**
153     * Set Curl option to set verify peer (verifies the domain's SSL cert)
154     *
155     * @param  bool $verify
156     * @return Curl
157     */
158    public function setVerifyPeer(bool $verify = true): Curl
159    {
160        $this->setOption(CURLOPT_SSL_VERIFYPEER, (bool)$verify);
161        return $this;
162    }
163
164    /**
165     * Set Curl option to set to allow self-signed certs
166     *
167     * @param  bool $allow
168     * @return Curl
169     */
170    public function allowSelfSigned(bool $allow = true): Curl
171    {
172        $this->setOption(CURLOPT_SSL_VERIFYHOST, (bool)$allow);
173        return $this;
174    }
175
176    /**
177     * Check if Curl is set to return transfer
178     *
179     * @return bool
180     */
181    public function isReturnTransfer(): bool
182    {
183        return (isset($this->options[CURLOPT_RETURNTRANSFER]) && ($this->options[CURLOPT_RETURNTRANSFER] == true));
184    }
185
186    /**
187     * Check if Curl is set to return header
188     *
189     * @return bool
190     */
191    public function isReturnHeader(): bool
192    {
193        return (isset($this->options[CURLOPT_HEADER]) && ($this->options[CURLOPT_HEADER] == true));
194    }
195
196    /**
197     * Check if Curl is set to verify peer
198     *
199     * @return bool
200     */
201    public function isVerifyPeer(): bool
202    {
203        return (isset($this->options[CURLOPT_SSL_VERIFYPEER]) && ($this->options[CURLOPT_SSL_VERIFYPEER] == true));
204    }
205
206    /**
207     * Check if Curl is set to allow self-signed certs
208     *
209     * @return bool
210     */
211    public function isAllowSelfSigned(): bool
212    {
213        return (isset($this->options[CURLOPT_SSL_VERIFYHOST]) && ($this->options[CURLOPT_SSL_VERIFYHOST] == true));
214    }
215
216    /**
217     * Return the Curl last info
218     *
219     * @param  ?int $opt
220     * @return array|string
221     */
222    public function getInfo(?int $opt = null): array|string
223    {
224        return ($opt !== null) ? curl_getinfo($this->resource, $opt) : curl_getinfo($this->resource);
225    }
226
227    /**
228     * Method to prepare the handler
229     *
230     * @param  Request $request
231     * @param  ?Auth   $auth
232     * @param  bool   $forceCustom
233     * @param  bool   $clear
234     * @throws Exception|\Pop\Http\Exception
235     * @return Curl
236     */
237    public function prepare(Request $request, ?Auth $auth = null, bool $forceCustom = false, bool $clear = true): Curl
238    {
239        $this->setMethod($request->getMethod(), $forceCustom);
240
241        // Clear headers for a fresh request based on the headers in the request object,
242        // else fall back to pre-defined headers in the stream context
243        if (($clear) && $this->hasOption(CURLOPT_HTTPHEADER)) {
244            $this->setOption(CURLOPT_HTTPHEADER, []);
245        }
246
247        // Add auth header
248        if ($auth !== null) {
249            $request->addHeader($auth->createAuthHeader());
250        }
251
252        $queryString = null;
253
254        // If request has data
255        if ($request->hasData()) {
256            $request->prepareData();
257            if (!($request->isGet()) && ($request->hasDataContent())) {
258                $this->setOption(CURLOPT_POSTFIELDS, $request->getDataContent());
259            } else if ($this->hasOption(CURLOPT_POSTFIELDS)) {
260                $this->removeOption(CURLOPT_POSTFIELDS);
261            }
262        // Else, if request has query
263        } else if ($request->hasQuery()) {
264            $queryString = '?' . http_build_query($request->getQuery());
265        // Else, if request has raw body content
266        } else if ($request->hasBodyContent()) {
267            $request->addHeader('Content-Length', $request->getBodyContentLength());
268            $this->setOption(CURLOPT_POSTFIELDS, $request->getBodyContent());
269        }
270
271        if ($request->hasHeaders()) {
272            $headers = [];
273
274            foreach ($request->getHeaders() as $header) {
275                $headers[] = $header->render();
276            }
277            if ($this->hasOption(CURLOPT_HTTPHEADER)) {
278                $customHeaders = $this->getOption(CURLOPT_HTTPHEADER);
279                foreach ($customHeaders as $customHeader) {
280                    if (!in_array($customHeader, $headers)) {
281                        $headers[] = $customHeader;
282                    }
283                }
284            }
285            $this->setOption(CURLOPT_HTTPHEADER, $headers);
286        }
287
288        $this->uri = $request->getUriAsString();
289        if (!empty($queryString) && !str_contains($this->uri, '?')) {
290            $this->uri .= $queryString;
291        }
292
293        $this->setOption(CURLOPT_URL, $this->uri);
294
295        return $this;
296    }
297
298    /**
299     * Method to send the request
300     *
301     * @throws Exception
302     * @return Response
303     */
304    public function send(): Response
305    {
306        $this->response = curl_exec($this->resource);
307
308        if ($this->response === false) {
309            throw new Exception('Error: ' . curl_errno($this->resource) . ' => ' . curl_error($this->resource) . '.');
310        }
311
312        return $this->parseResponse();
313    }
314
315    /**
316     * Parse the response
317     *
318     * @return Response
319     */
320    public function parseResponse(): Response
321    {
322        $response = new Response();
323
324        // If the CURLOPT_RETURNTRANSFER option is set, get the response body and parse the headers.
325        if (isset($this->options[CURLOPT_RETURNTRANSFER]) && ($this->options[CURLOPT_RETURNTRANSFER])) {
326            $headerSize = $this->getInfo(CURLINFO_HEADER_SIZE);
327            if (isset($this->options[CURLOPT_HEADER]) && ($this->options[CURLOPT_HEADER])) {
328                $parsedHeaders = Parser::parseHeaders(substr($this->response, 0, $headerSize));
329                $response->setVersion($parsedHeaders['version']);
330                $response->setCode($parsedHeaders['code']);
331                $response->setMessage($parsedHeaders['message']);
332                $response->addHeaders($parsedHeaders['headers']);
333                $response->setBody(substr($this->response, $headerSize));
334            } else {
335                $response->setBody($this->response);
336            }
337        }
338
339        if ($response->hasHeader('Content-Encoding')) {
340            $response->decodeBodyContent();
341        }
342
343        return $response;
344    }
345
346    /**
347     * Method to reset the handler
348     *
349     * @param  bool $default
350     * @return Curl
351     */
352    public function reset(bool $default = true): Curl
353    {
354        curl_reset($this->resource);
355        $this->response = null;
356        $this->options  = [];
357
358        if ($default) {
359            $this->setOption(CURLOPT_HEADER, true);
360            $this->setOption(CURLOPT_RETURNTRANSFER, true);
361        }
362
363        return $this;
364    }
365
366    /**
367     * Close the handler connection
368     *
369     * @return void
370     */
371    public function disconnect(): void
372    {
373        if ($this->hasResource()) {
374            curl_close($this->resource);
375            $this->resource = null;
376            $this->response = null;
377            $this->options  = [];
378        }
379    }
380
381}