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