Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
91.67% |
121 / 132 |
|
88.89% |
24 / 27 |
CRAP | |
0.00% |
0 / 1 |
Stream | |
91.67% |
121 / 132 |
|
88.89% |
24 / 27 |
84.80 | |
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% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
prepare | |
81.08% |
30 / 37 |
|
0.00% |
0 / 1 |
33.31 | |||
send | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
parseResponse | |
87.50% |
21 / 24 |
|
0.00% |
0 / 1 |
9.16 | |||
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\AbstractRequest; |
17 | use Pop\Http\Auth; |
18 | use Pop\Http\Parser; |
19 | use Pop\Http\Client\Request; |
20 | use Pop\Http\Client\Response; |
21 | use 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 | */ |
33 | class 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 | } |