Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.98% covered (success)
88.98%
105 / 118
85.71% covered (success)
85.71%
18 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
Stream
88.98% covered (success)
88.98%
105 / 118
85.71% covered (success)
85.71%
18 / 21
63.65
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 setMethod
100.00% covered (success)
100.00%
5 / 5
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
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 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
 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
2
 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
2
 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
 open
93.75% covered (success)
93.75%
45 / 48
0.00% covered (danger)
0.00%
0 / 1
21.11
 send
85.00% covered (success)
85.00%
17 / 20
0.00% covered (danger)
0.00%
0 / 1
5.08
 reset
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 disconnect
100.00% covered (success)
100.00%
3 / 3
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-2023 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;
15
16use Pop\Http\Parser;
17use Pop\Mime\Message;
18
19/**
20 * HTTP stream client class
21 *
22 * @category   Pop
23 * @package    Pop\Http
24 * @author     Nick Sagona, III <dev@nolainteractive.com>
25 * @copyright  Copyright (c) 2009-2023 NOLA Interactive, LLC. (http://www.nolainteractive.com)
26 * @license    http://www.popphp.org/license     New BSD License
27 * @version    4.1.0
28 */
29class Stream extends AbstractClient
30{
31
32    /**
33     * Stream context
34     * @var resource
35     */
36    protected $context = null;
37
38    /**
39     * Stream context options
40     * @var array
41     */
42    protected $contextOptions = [];
43
44    /**
45     * Stream context parameters
46     * @var array
47     */
48    protected $contextParams = [];
49
50    /**
51     * Stream mode
52     * @var string
53     */
54    protected $mode = 'r';
55
56    /**
57     * HTTP response headers
58     * @var array
59     */
60    protected $httpResponseHeaders = null;
61
62    /**
63     * Constructor
64     *
65     * Instantiate the stream object
66     *
67     * @param  string $url
68     * @param  string $method
69     * @param  string $mode
70     * @param  array  $options
71     * @param  array  $params
72     */
73    public function __construct($url = null, $method = 'GET', $mode = 'r', array $options = [], array $params = [])
74    {
75        parent::__construct($url, $method);
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     * Set the method
89     *
90     * @param  string  $method
91     * @param  boolean $strict
92     * @throws Exception
93     * @return Stream
94     */
95    public function setMethod($method, $strict = true)
96    {
97        parent::setMethod($method, $strict);
98
99        if (!isset($this->contextOptions['http'])) {
100            $this->contextOptions['http'] = [];
101        }
102
103        $this->contextOptions['http']['method'] = $this->method;
104
105        return $this;
106    }
107
108    /**
109     * Return stream resource (alias to $this->getResource())
110     *
111     * @return resource
112     */
113    public function stream()
114    {
115        return $this->resource;
116    }
117
118    /**
119     * Create stream context
120     *
121     * @return Stream
122     */
123    public function createContext()
124    {
125        if ((count($this->contextOptions) > 0) && (count($this->contextParams) > 0)) {
126            $this->context = stream_context_create($this->contextOptions, $this->contextParams);
127        } else if (count($this->contextOptions) > 0) {
128            $this->context = stream_context_create($this->contextOptions);
129        } else {
130            $this->context = stream_context_create();
131        }
132
133        return $this;
134    }
135
136    /**
137     * Add a context options
138     *
139     * @param  string $name
140     * @param  mixed  $option
141     * @return Stream
142     */
143    public function addContextOption($name, $option)
144    {
145        if (isset($this->contextOptions[$name]) && is_array($this->contextOptions[$name]) && is_array($option)) {
146            $this->contextOptions[$name] = array_merge($this->contextOptions[$name], $option);
147        } else {
148            $this->contextOptions[$name] = $option;
149        }
150
151        return $this;
152    }
153
154    /**
155     * Add a context parameter
156     *
157     * @param  string $name
158     * @param  mixed  $param
159     * @return Stream
160     */
161    public function addContextParam($name, $param)
162    {
163        $this->contextParams[$name] = $param;
164        return $this;
165    }
166
167    /**
168     * Set the context options
169     *
170     * @param  array $options
171     * @return Stream
172     */
173    public function setContextOptions(array $options)
174    {
175        foreach ($options as $name => $option) {
176            $this->addContextOption($name, $option);
177        }
178        return $this;
179    }
180
181    /**
182     * Set the context parameters
183     *
184     * @param  array $params
185     * @return Stream
186     */
187    public function setContextParams(array $params)
188    {
189        foreach ($params as $name => $param) {
190            $this->addContextParam($name, $param);
191        }
192        return $this;
193    }
194
195    /**
196     * Set the mode
197     *
198     * @param  string $mode
199     * @return Stream
200     */
201    public function setMode($mode)
202    {
203        $this->mode = $mode;
204        return $this;
205    }
206
207    /**
208     * Get the context resource
209     *
210     * @return resource
211     */
212    public function getContext()
213    {
214        return $this->context;
215    }
216
217    /**
218     * Get the context options
219     *
220     * @return array
221     */
222    public function getContextOptions()
223    {
224        return $this->contextOptions;
225    }
226
227    /**
228     * Get a context option
229     *
230     * @param  string $name
231     * @return mixed
232     */
233    public function getContextOption($name)
234    {
235        return (isset($this->contextOptions[$name])) ? $this->contextOptions[$name] : null;
236    }
237
238    /**
239     * Determine if a context option has been set
240     *
241     * @param  string $name
242     * @return boolean
243     */
244    public function hasContextOption($name)
245    {
246        return (isset($this->contextOptions[$name]));
247    }
248
249    /**
250     * Get the context parameters
251     *
252     * @return array
253     */
254    public function getContextParams()
255    {
256        return $this->contextParams;
257    }
258
259    /**
260     * Get a context parameter
261     *
262     * @param  string $name
263     * @return mixed
264     */
265    public function getContextParam($name)
266    {
267        return (isset($this->contextParams[$name])) ? $this->contextParams[$name] : null;
268    }
269
270    /**
271     * Determine if a context parameter has been set
272     *
273     * @param  string $name
274     * @return boolean
275     */
276    public function hasContextParam($name)
277    {
278        return (isset($this->contextParams[$name]));
279    }
280
281    /**
282     * Get the mode
283     *
284     * @return string
285     */
286    public function getMode()
287    {
288        return $this->mode;
289    }
290
291    /**
292     * Create and open stream resource
293     *
294     * @return Stream
295     */
296    public function open()
297    {
298        $url                  = $this->url;
299        $http_response_header = null;
300
301        if (isset($this->contextOptions['http']['header'])) {
302            $this->contextOptions['http']['header'] = null;
303        }
304
305        if (null !== $this->request) {
306            // Set query data if there is any
307            if ($this->request->hasFields()) {
308                // Append GET query string to URL
309                if (($this->method == 'GET') && ((!$this->request->hasHeader('Content-Type')) ||
310                        ($this->request->getHeaderValue('Content-Type') == 'application/x-www-form-urlencoded'))) {
311                    $url .= '?' . $this->request->getQuery();
312                    if (!$this->request->hasHeader('Content-Type')) {
313                        $this->request->addHeader('Content-Type', 'application/x-www-form-urlencoded');
314                    }
315                // Else, prepare request data for transmission
316                } else {
317                    // If request is JSON
318                    if ($this->request->getHeaderValue('Content-Type') == 'application/json') {
319                        $content = json_encode($this->request->getFields(), JSON_PRETTY_PRINT);
320                        $this->request->addHeader('Content-Length', strlen($content));
321                        $this->contextOptions['http']['content'] = $content;
322                    // If request is a URL-encoded form
323                    } else if ($this->request->getHeaderValue('Content-Type') == 'application/x-www-form-urlencoded') {
324                        $this->request->addHeader('Content-Length', $this->request->getQueryLength());
325                        $this->contextOptions['http']['content'] = $this->request->getQuery();
326                    // Else, if request is a multipart form
327                    } else if ($this->request->isMultipartForm()) {
328                        $formMessage = Message::createForm($this->request->getFields());
329                        $header      = $formMessage->getHeader('Content-Type');
330                        $content     = $formMessage->render(false);
331                        $formMessage->removeHeader('Content-Type');
332                        $this->request->addHeader($header)
333                            ->addHeader('Content-Length', strlen($content));
334                        $this->contextOptions['http']['content'] = $content;
335                    // Else, basic request with data
336                    } else {
337                        $this->contextOptions['http']['content'] = $this->request->getQuery();
338                        if (!$this->request->hasHeader('Content-Type')) {
339                            $this->request->addHeader('Content-Type', 'application/x-www-form-urlencoded');
340                        }
341                    }
342                }
343            // Else, if there is raw body content
344            } else if ($this->request->hasBodyContent()) {
345                $this->request->addHeader('Content-Length', strlen($this->request->getBodyContent()));
346                $this->contextOptions['http']['content'] = $this->request->getBodyContent();
347            }
348
349            if ($this->request->hasHeaders()) {
350                $headers = [];
351
352                foreach ($this->request->getHeaders() as $header => $value) {
353                    if (is_array($value)) {
354                        foreach ($value as $hdr => $val) {
355                            $headers[] = (string)$val;
356                        }
357                    } else {
358                        $headers[] = (string)$value;
359                    }
360                }
361
362                if (isset($this->contextOptions['http']['header'])) {
363                    $this->contextOptions['http']['header'] .= "\r\n" . implode("\r\n", $headers) . "\r\n";
364                } else {
365                    $this->contextOptions['http']['header'] = implode("\r\n", $headers) . "\r\n";
366                }
367            }
368        }
369
370        if ((count($this->contextOptions) > 0) || (count($this->contextParams) > 0)) {
371            $this->createContext();
372        }
373
374        $this->resource = (null !== $this->context) ?
375            @fopen($url, $this->mode, false, $this->context) : @fopen($url, $this->mode);
376
377        $this->httpResponseHeaders = $http_response_header;
378
379        return $this;
380    }
381
382    /**
383     * Method to send the request and get the response
384     *
385     * @return void
386     */
387    public function send()
388    {
389        $rawHeader = null;
390        $headers   = [];
391        $body      = null;
392
393        $this->open();
394
395        if (null === $this->response) {
396            $this->response = new Response();
397        }
398
399        if ($this->resource !== false) {
400            $meta      = stream_get_meta_data($this->resource);
401            $headers   = $meta['wrapper_data'];
402            $body      = stream_get_contents($this->resource);
403        } else if (null !== $this->httpResponseHeaders) {
404            $headers   = $this->httpResponseHeaders;
405        }
406
407        // Parse response headers
408        $parsedHeaders = Parser::parseHeaders($headers);
409        $this->response->setVersion($parsedHeaders['version']);
410        $this->response->setCode($parsedHeaders['code']);
411        $this->response->setMessage($parsedHeaders['message']);
412        $this->response->addHeaders($parsedHeaders['headers']);
413        $this->response->setBody($body);
414
415        if ($this->response->hasHeader('Content-Encoding')) {
416            $this->response->decodeBodyContent();
417        }
418    }
419
420    /**
421     * Method to reset the client object
422     *
423     * @return Stream
424     */
425    public function reset()
426    {
427        $this->request             = new Request();
428        $this->response            = new Response();
429        $this->context             = null;
430        $this->contextOptions      = [];
431        $this->contextParams       = [];
432        $this->httpResponseHeaders = null;
433
434        return $this;
435    }
436
437    /**
438     * Close the stream
439     *
440     * @return void
441     */
442    public function disconnect()
443    {
444        if ($this->hasResource()) {
445            $this->resource = null;
446            $this->context  = null;
447        }
448    }
449
450}