Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.94% covered (success)
91.94%
114 / 124
88.89% covered (success)
88.89%
24 / 27
CRAP
0.00% covered (danger)
0.00%
0 / 1
Stream
91.94% covered (success)
91.94%
114 / 124
88.89% covered (success)
88.89%
24 / 27
70.43
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 create
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setMethod
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 stream
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createContext
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 addContextOption
83.33% covered (success)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
6.17
 addContextParam
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setContextOptions
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setContextParams
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setMode
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%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getContext
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getContextOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getContextOption
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasContextOption
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getContextParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getContextParam
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasContextParam
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isVerifyPeer
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 isAllowSelfSigned
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 prepare
81.82% covered (success)
81.82%
27 / 33
0.00% covered (danger)
0.00%
0 / 1
19.95
 send
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 parseResponse
84.21% covered (success)
84.21%
16 / 19
0.00% covered (danger)
0.00%
0 / 1
5.10
 reset
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 disconnect
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
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 stream 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 Stream extends AbstractHandler
33{
34
35    /**
36     * Stream context
37     * @var mixed
38     */
39    protected mixed $context = null;
40
41    /**
42     * Stream context options
43     * @var array
44     */
45    protected array $contextOptions = [];
46
47    /**
48     * Stream context parameters
49     * @var array
50     */
51    protected array $contextParams = [];
52
53    /**
54     * Stream mode
55     * @var string
56     */
57    protected string $mode = 'r';
58
59    /**
60     * HTTP response headers
61     * @var ?array
62     */
63    protected ?array $httpResponseHeaders = null;
64
65    /**
66     * Constructor
67     *
68     * Instantiate the stream handler object
69     *
70     * @param string $mode
71     * @param array  $options
72     * @param array  $params
73     */
74    public function __construct(string $mode = 'r', array $options = [], array $params = [])
75    {
76        $this->setMode($mode);
77
78        if (count($options) > 0) {
79            $this->setContextOptions($options);
80        }
81        if (count($params) > 0) {
82            $this->setContextParams($params);
83        }
84    }
85
86    /**
87     * Factory method to create a Curl client
88     *
89     * @param  string $mode
90     * @param  array  $options
91     * @param  array  $params
92     * @return Stream
93     */
94    public static function create(string $method = 'GET', string $mode = 'r', array $options = [], array $params = []): Stream
95    {
96        $handler = new self($mode, $options, $params);
97        $handler->setMethod($method);
98        return $handler;
99    }
100
101    /**
102     * Set the method
103     *
104     * @param  string $method
105     * @return Stream
106     */
107    public function setMethod(string $method): Stream
108    {
109        if (!isset($this->contextOptions['http'])) {
110            $this->contextOptions['http'] = [];
111        }
112
113        $this->contextOptions['http']['method'] = $method;
114
115        return $this;
116    }
117
118    /**
119     * Return stream resource (alias to $this->getResource())
120     *
121     * @return mixed
122     */
123    public function stream(): mixed
124    {
125        return $this->resource;
126    }
127
128    /**
129     * Create stream context
130     *
131     * @return Stream
132     */
133    public function createContext(): Stream
134    {
135        if ((count($this->contextOptions) > 0) && (count($this->contextParams) > 0)) {
136            $this->context = stream_context_create($this->contextOptions, $this->contextParams);
137        } else if (count($this->contextOptions) > 0) {
138            $this->context = stream_context_create($this->contextOptions);
139        } else {
140            $this->context = stream_context_create();
141        }
142
143        return $this;
144    }
145
146    /**
147     * Add a context options
148     *
149     * @param  string $name
150     * @param  mixed  $option
151     * @return Stream
152     */
153    public function addContextOption(string $name, mixed $option): Stream
154    {
155        if (isset($this->contextOptions[$name]) && is_array($this->contextOptions[$name]) && is_array($option)) {
156            $this->contextOptions[$name] = array_merge($this->contextOptions[$name], $option);
157        } else {
158            $this->contextOptions[$name] = $option;
159        }
160
161        if (isset($this->contextOptions['http']) && isset($this->contextOptions['http']['protocol_version'])) {
162            $this->httpVersion = $this->contextOptions['http']['protocol_version'];
163        }
164
165        return $this;
166    }
167
168    /**
169     * Add a context parameter
170     *
171     * @param  string $name
172     * @param  mixed  $param
173     * @return Stream
174     */
175    public function addContextParam(string $name, mixed $param): Stream
176    {
177        $this->contextParams[$name] = $param;
178        return $this;
179    }
180
181    /**
182     * Set the context options
183     *
184     * @param  array $options
185     * @return Stream
186     */
187    public function setContextOptions(array $options): Stream
188    {
189        foreach ($options as $name => $option) {
190            $this->addContextOption($name, $option);
191        }
192        return $this;
193    }
194
195    /**
196     * Set the context parameters
197     *
198     * @param  array $params
199     * @return Stream
200     */
201    public function setContextParams(array $params): Stream
202    {
203        foreach ($params as $name => $param) {
204            $this->addContextParam($name, $param);
205        }
206        return $this;
207    }
208
209    /**
210     * Set the mode
211     *
212     * @param  string $mode
213     * @return Stream
214     */
215    public function setMode(string $mode): Stream
216    {
217        $this->mode = $mode;
218        return $this;
219    }
220
221    /**
222     * Set Stream context option to set verify peer (verifies the domain's SSL cert)
223     *
224     * @param  bool $verify
225     * @return Stream
226     */
227    public function setVerifyPeer(bool $verify = true): Stream
228    {
229        $this->addContextOption('ssl', ['verify_peer' => (bool)$verify]);
230        return $this;
231    }
232
233    /**
234     * Set Stream context option to set to allow self-signed certs (verify host)
235     *
236     * @param  bool $allow
237     * @return Stream
238     */
239    public function allowSelfSigned(bool $allow = true): Stream
240    {
241        $this->addContextOption('ssl', ['allow_self_signed' => (bool)$allow]);
242        if (!$allow) {
243            $this->addContextOption('ssl', ['verify_peer' => false]);
244        }
245        return $this;
246    }
247
248    /**
249     * Get the context resource
250     *
251     * @return mixed
252     */
253    public function getContext(): mixed
254    {
255        return $this->context;
256    }
257
258    /**
259     * Get the context options
260     *
261     * @return array
262     */
263    public function getContextOptions(): array
264    {
265        return $this->contextOptions;
266    }
267
268    /**
269     * Get a context option
270     *
271     * @param  string $name
272     * @return mixed
273     */
274    public function getContextOption(string $name): mixed
275    {
276        return $this->contextOptions[$name] ?? null;
277    }
278
279    /**
280     * Determine if a context option has been set
281     *
282     * @param  string $name
283     * @return bool
284     */
285    public function hasContextOption(string $name): bool
286    {
287        return (isset($this->contextOptions[$name]));
288    }
289
290    /**
291     * Get the context parameters
292     *
293     * @return array
294     */
295    public function getContextParams(): array
296    {
297        return $this->contextParams;
298    }
299
300    /**
301     * Get a context parameter
302     *
303     * @param  string $name
304     * @return mixed
305     */
306    public function getContextParam(string $name): mixed
307    {
308        return $this->contextParams[$name] ?? null;
309    }
310
311    /**
312     * Determine if a context parameter has been set
313     *
314     * @param  string $name
315     * @return bool
316     */
317    public function hasContextParam(string $name): bool
318    {
319        return (isset($this->contextParams[$name]));
320    }
321
322    /**
323     * Get the mode
324     *
325     * @return string
326     */
327    public function getMode(): string
328    {
329        return $this->mode;
330    }
331
332    /**
333     * Check if Stream is set to verify peer
334     *
335     * @return bool
336     */
337    public function isVerifyPeer(): bool
338    {
339        return (isset($this->contextOptions['ssl']) && isset($this->contextOptions['ssl']['verify_peer']) &&
340            ($this->contextOptions['ssl']['verify_peer'] == true));
341    }
342
343    /**
344     * Check if Stream is set to allow self-signed certs
345     *
346     * @return bool
347     */
348    public function isAllowSelfSigned(): bool
349    {
350        return (isset($this->contextOptions['ssl']) && isset($this->contextOptions['ssl']['allow_self_signed']) &&
351            ($this->contextOptions['ssl']['allow_self_signed'] == true));
352    }
353
354    /**
355     * Method to prepare the handler
356     *
357     * @param  Request $request
358     * @param  ?Auth   $auth
359     * @param  bool    $clear
360     * @throws Exception|\Pop\Http\Exception
361     * @return Stream
362     */
363    public function prepare(Request $request, ?Auth $auth = null, bool $clear = true): Stream
364    {
365        $this->setMethod($request->getMethod());
366
367        // Clear headers for a fresh request based on the headers in the request object,
368        // else fall back to pre-defined headers in the stream context
369        if (($clear) && isset($this->contextOptions['http']['header'])) {
370            $this->contextOptions['http']['header'] = null;
371        }
372
373        // Add auth header
374        if ($auth !== null) {
375            $request->addHeader($auth->createAuthHeader());
376        }
377
378        $queryString = null;
379
380        // If request has data
381        if ($request->hasData()) {
382            $request->prepareData();
383            if ($request->hasDataContent()) {
384                $this->contextOptions['http']['content'] = $request->getDataContent();
385            } else if (!empty($this->contextOptions['http']['content'])) {
386                unset($this->contextOptions['http']['content']);
387            }
388        // Else, if request has query
389        } else if ($request->hasQuery()) {
390            $queryString = '?' . http_build_query($request->getQuery());
391        // Else, if there is raw body content
392        } else if ($request->hasBodyContent()) {
393            $request->addHeader('Content-Length', $request->getBodyContentLength());
394            $this->contextOptions['http']['content'] = $request->getBodyContent();
395        }
396
397        if ($request->hasHeaders()) {
398            $headers = [];
399
400            foreach ($request->getHeaders() as $value) {
401                if (is_array($value)) {
402                    foreach ($value as $val) {
403                        $headers[] = (string)$val;
404                    }
405                } else {
406                    $headers[] = (string)$value;
407                }
408            }
409
410            if (isset($this->contextOptions['http']['header'])) {
411                $this->contextOptions['http']['header'] .= "\r\n" . implode("\r\n", $headers) . "\r\n";
412            } else {
413                $this->contextOptions['http']['header'] = implode("\r\n", $headers) . "\r\n";
414            }
415        }
416
417        if ((count($this->contextOptions) > 0) || (count($this->contextParams) > 0)) {
418            $this->createContext();
419        }
420
421        $this->uri = $request->getUriAsString();
422        if (!empty($queryString) && !str_contains($this->uri, '?')) {
423            $this->uri .= $queryString;
424        }
425
426        return $this;
427    }
428
429    /**
430     * Method to send the request
431     *
432     * @throws Exception
433     * @return Response
434     */
435    public function send(): Response
436    {
437        if ($this->uri === null) {
438            throw new Exception('Error: The request handler has not been prepared.');
439        }
440        $http_response_header = null;
441
442        $this->resource = ($this->context !== null) ?
443            @fopen($this->uri, $this->mode, false, $this->context) : @fopen($this->uri, $this->mode);
444
445        $this->uri = null;
446        $this->httpResponseHeaders = $http_response_header;
447
448        return $this->parseResponse();
449    }
450
451    /**
452     * Parse the response
453     *
454     * @return Response
455     */
456    public function parseResponse(): Response
457    {
458        $response = new Response();
459        $headers  = [];
460        $body     = null;
461
462        if ($this->resource !== false) {
463            $meta    = stream_get_meta_data($this->resource);
464            $headers = $meta['wrapper_data'];
465            $body    = stream_get_contents($this->resource);
466        } else if ($this->httpResponseHeaders !== null) {
467            $headers = $this->httpResponseHeaders;
468        }
469
470        // Parse response headers
471        $parsedHeaders = Parser::parseHeaders($headers);
472        $response->setVersion($parsedHeaders['version']);
473        $response->setCode($parsedHeaders['code']);
474        $response->setMessage($parsedHeaders['message']);
475        $response->addHeaders($parsedHeaders['headers']);
476        if ($body !== null) {
477            $response->setBody($body);
478        }
479
480        if ($response->hasHeader('Content-Encoding')) {
481            $response->decodeBodyContent();
482        }
483
484        return $response;
485    }
486
487    /**
488     * Method to reset the handler
489     *
490     * @return Stream
491     */
492    public function reset(): Stream
493    {
494        $this->context             = null;
495        $this->contextOptions      = [];
496        $this->contextParams       = [];
497        $this->httpResponseHeaders = null;
498        return $this;
499    }
500
501    /**
502     * Close the handler connection
503     *
504     * @return void
505     */
506    public function disconnect(): void
507    {
508        $this->uri                 = null;
509        $this->resource            = null;
510        $this->context             = null;
511        $this->contextOptions      = [];
512        $this->contextParams       = [];
513        $this->httpResponseHeaders = null;
514    }
515
516}