Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
91.94% |
114 / 124 |
|
88.89% |
24 / 27 |
CRAP | |
0.00% |
0 / 1 |
Stream | |
91.94% |
114 / 124 |
|
88.89% |
24 / 27 |
70.43 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
create | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
setMethod | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
stream | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
createContext | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
addContextOption | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
6.17 | |||
addContextParam | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setContextOptions | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
setContextParams | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
setMode | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setVerifyPeer | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
allowSelfSigned | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getContext | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getContextOptions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getContextOption | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasContextOption | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getContextParams | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getContextParam | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasContextParam | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getMode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isVerifyPeer | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
3 | |||
isAllowSelfSigned | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
3 | |||
prepare | |
81.82% |
27 / 33 |
|
0.00% |
0 / 1 |
19.95 | |||
send | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
parseResponse | |
84.21% |
16 / 19 |
|
0.00% |
0 / 1 |
5.10 | |||
reset | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
disconnect | |
100.00% |
6 / 6 |
|
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 | */ |
14 | namespace Pop\Http\Client\Handler; |
15 | |
16 | use Pop\Http\Auth; |
17 | use Pop\Http\Parser; |
18 | use Pop\Http\Client\Request; |
19 | use Pop\Http\Client\Response; |
20 | use 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 | */ |
32 | class 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 | } |