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