Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
97.67% |
126 / 129 |
|
85.71% |
6 / 7 |
CRAP | |
0.00% |
0 / 1 |
Parser | |
97.67% |
126 / 129 |
|
85.71% |
6 / 7 |
48 | |
0.00% |
0 / 1 |
parseHeaders | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
6 | |||
parseDataByContentType | |
100.00% |
31 / 31 |
|
100.00% |
1 / 1 |
17 | |||
parseResponseFromUri | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
parseResponseFromString | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
2 | |||
encodeData | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
7 | |||
decodeData | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
9 | |||
decodeChunkedData | |
85.00% |
17 / 20 |
|
0.00% |
0 / 1 |
6.12 |
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; |
15 | |
16 | use Pop\Mime\Message; |
17 | use Pop\Mime\Part\Header; |
18 | |
19 | /** |
20 | * HTTP response parser class |
21 | * |
22 | * @category Pop |
23 | * @package Pop\Http |
24 | * @author Nick Sagona, III <dev@nolainteractive.com> |
25 | * @copyright Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com) |
26 | * @license http://www.popphp.org/license New BSD License |
27 | * @version 5.2.0 |
28 | */ |
29 | class Parser |
30 | { |
31 | |
32 | /** |
33 | * Encoding constants |
34 | * @var string |
35 | */ |
36 | const BASE64 = 'BASE64'; |
37 | const QUOTED = 'QUOTED'; |
38 | const URL = 'URL'; |
39 | const RAW_URL = 'RAW_URL'; |
40 | const GZIP = 'GZIP'; |
41 | const DEFLATE = 'DEFLATE'; |
42 | |
43 | /** |
44 | * Parse headers |
45 | * |
46 | * @param mixed $headers |
47 | * @return array |
48 | */ |
49 | public static function parseHeaders(mixed $headers): array |
50 | { |
51 | $httpVersion = null; |
52 | $httpCode = null; |
53 | $httpMessage = null; |
54 | $headerObjects = []; |
55 | |
56 | $headers = (is_string($headers)) ? |
57 | array_map('trim', explode("\n", $headers)) : (array)$headers; |
58 | |
59 | foreach ($headers as $header) { |
60 | if (str_contains($header, 'HTTP/')) { |
61 | $httpVersion = substr($header, 0, strpos($header, ' ')); |
62 | $httpVersion = substr($httpVersion, (strpos($httpVersion, '/') + 1)); |
63 | |
64 | $match = []; |
65 | preg_match('/\d\d\d/', trim($header), $match); |
66 | |
67 | if (isset($match[0])) { |
68 | $httpCode = $match[0]; |
69 | $httpMessage = trim(substr($header, strpos($header, ' ' . $httpCode . ' ') + 5)); |
70 | } |
71 | } else if (str_contains($header, ':')) { |
72 | $headerObject = Header::parse($header); |
73 | $headerObjects[$headerObject->getName()] = $headerObject; |
74 | } |
75 | } |
76 | |
77 | return [ |
78 | 'version' => $httpVersion, |
79 | 'code' => $httpCode, |
80 | 'message' => $httpMessage, |
81 | 'headers' => $headerObjects |
82 | ]; |
83 | } |
84 | |
85 | /** |
86 | * Parse request or response data based on content type |
87 | * |
88 | * @param string $rawData |
89 | * @param ?string $contentType |
90 | * @param ?string $encoding |
91 | * @param bool $chunked |
92 | * @return mixed |
93 | */ |
94 | public static function parseDataByContentType( |
95 | string $rawData, ?string $contentType = null, ?string $encoding = null, bool $chunked = false |
96 | ): mixed |
97 | { |
98 | $parsedResult = false; |
99 | |
100 | if ($contentType !== null) { |
101 | $contentType = strtolower($contentType); |
102 | } |
103 | if ($encoding !== null) { |
104 | $encoding = strtoupper($encoding); |
105 | } |
106 | |
107 | // JSON data |
108 | if (($contentType !== null) && (str_contains($contentType, 'json'))) { |
109 | $parsedResult = json_decode(self::decodeData($rawData, $encoding, $chunked), true); |
110 | // XML data |
111 | } else if (($contentType !== null) && (str_contains($contentType, 'xml'))) { |
112 | $rawData = self::decodeData($rawData, $encoding, $chunked); |
113 | $matches = []; |
114 | preg_match_all('/<!\[cdata\[(.*?)\]\]>/is', $rawData, $matches); |
115 | |
116 | foreach ($matches[0] as $match) { |
117 | $strip = str_replace( |
118 | ['<![CDATA[', ']]>', '<', '>'], |
119 | ['', '', '<', '>'], |
120 | $match |
121 | ); |
122 | $rawData = str_replace($match, $strip, $rawData); |
123 | } |
124 | |
125 | $parsedResult = json_decode(json_encode((array)simplexml_load_string($rawData)), true); |
126 | // URL-encoded form data |
127 | } else if (($contentType !== null) && (str_contains($contentType, 'application/x-www-form-urlencoded'))) { |
128 | $parsedResult = []; |
129 | parse_str(self::decodeData($rawData, $encoding, $chunked), $parsedResult); |
130 | // Multipart form data |
131 | } else if (($contentType !== null) && (str_contains($contentType, 'multipart/form-data'))) { |
132 | $formContent = (!str_contains($rawData, 'Content-Type:')) ? |
133 | 'Content-Type: ' . $contentType . "\r\n\r\n" . $rawData : $rawData; |
134 | $parsedResult = Message::parseForm($formContent); |
135 | // HTML text or plain text |
136 | } else if (($contentType !== null) && (str_contains($contentType, 'text/html') || str_contains($contentType, 'text/plain'))) { |
137 | $parsedResult = $rawData; |
138 | // Fallback to just the encoding |
139 | } else if ($encoding !== null) { |
140 | $parsedResult = self::decodeData($rawData, $encoding, $chunked); |
141 | } |
142 | |
143 | return $parsedResult; |
144 | } |
145 | |
146 | /** |
147 | * Parse a response from a URI |
148 | * |
149 | * @param string $uri |
150 | * @param string $method |
151 | * @param string $mode |
152 | * @param array $options |
153 | * @param array $params |
154 | * @throws Client\Exception|Exception |
155 | * @return Server\Response |
156 | */ |
157 | public static function parseResponseFromUri( |
158 | string $uri, string $method = 'GET', string $mode = 'r', array $options = [], array $params = [] |
159 | ): Server\Response |
160 | { |
161 | $request = new Client\Request($uri, $method); |
162 | $handler = new Client\Handler\Stream($mode, $options, $params); |
163 | $response = $handler->prepare($request, null, false)->send(); |
164 | |
165 | return new Server\Response([ |
166 | 'code' => $response->getCode(), |
167 | 'headers' => $response->getHeaders(), |
168 | 'body' => $response->getBody(), |
169 | 'message' => $response->getMessage(), |
170 | 'version' => $response->getVersion() |
171 | ]); |
172 | } |
173 | |
174 | /** |
175 | * Parse a response from a full response string |
176 | * |
177 | * @param string $responseString |
178 | * @return Server\Response |
179 | */ |
180 | public static function parseResponseFromString(string $responseString): Server\Response |
181 | { |
182 | $headerString = substr($responseString, 0, strpos($responseString, "\r\n\r\n")); |
183 | $bodyString = substr($responseString, (strpos($responseString, "\r\n\r\n") + 4)); |
184 | $parsedHeaders = self::parseHeaders($headerString); |
185 | |
186 | // If the body content is encoded, decode the body content |
187 | if (array_key_exists('Content-Encoding', $parsedHeaders['headers'])) { |
188 | $encoding = strtoupper($parsedHeaders['headers']['Content-Encoding']); |
189 | $chunked = ($parsedHeaders['headers']['Transfer-Encoding'] == 'chunked'); |
190 | $body = self::decodeData($bodyString, $encoding, $chunked); |
191 | } else { |
192 | $body = $bodyString; |
193 | } |
194 | |
195 | return new Server\Response([ |
196 | 'code' => $parsedHeaders['code'], |
197 | 'headers' => $parsedHeaders['headers'], |
198 | 'body' => $body, |
199 | 'message' => $parsedHeaders['message'], |
200 | 'version' => $parsedHeaders['version'] |
201 | ]); |
202 | } |
203 | |
204 | /** |
205 | * Encode data |
206 | * |
207 | * @param string $data |
208 | * @param ?string $encoding |
209 | * @return string |
210 | */ |
211 | public static function encodeData(string $data, ?string $encoding = null): string |
212 | { |
213 | switch ($encoding) { |
214 | case self::BASE64: |
215 | $data = base64_encode($data); |
216 | break; |
217 | case self::QUOTED: |
218 | $data = quoted_printable_encode($data); |
219 | break; |
220 | case self::URL: |
221 | $data = urlencode($data); |
222 | break; |
223 | case self::RAW_URL: |
224 | $data = rawurlencode($data); |
225 | break; |
226 | case self::GZIP: |
227 | $data = gzencode($data); |
228 | break; |
229 | case self::DEFLATE: |
230 | $data = gzdeflate($data); |
231 | break; |
232 | } |
233 | |
234 | return $data; |
235 | } |
236 | |
237 | /** |
238 | * Decode data |
239 | * |
240 | * @param string $data |
241 | * @param ?string $encoding |
242 | * @param bool $chunked |
243 | * @return string |
244 | */ |
245 | public static function decodeData(string $data, ?string $encoding = null, bool $chunked = false): string |
246 | { |
247 | if ($chunked) { |
248 | $data = self::decodeChunkedData($data); |
249 | } |
250 | |
251 | switch ($encoding) { |
252 | case self::BASE64: |
253 | $data = base64_decode($data); |
254 | break; |
255 | case self::QUOTED: |
256 | $data = quoted_printable_decode($data); |
257 | break; |
258 | case self::URL: |
259 | $data = urldecode($data); |
260 | break; |
261 | case self::RAW_URL: |
262 | $data = rawurldecode($data); |
263 | break; |
264 | case self::GZIP: |
265 | $data = gzinflate(substr($data, 10)); |
266 | break; |
267 | case self::DEFLATE: |
268 | $zLib = unpack('n', substr($data, 0, 2)); |
269 | $data = ($zLib[1] % 31 == 0) ? gzuncompress($data) : gzinflate($data); |
270 | break; |
271 | } |
272 | |
273 | return $data; |
274 | } |
275 | |
276 | /** |
277 | * Decode a chunked transfer-encoded data and return the decoded data |
278 | * |
279 | * @param string $data |
280 | * @return string |
281 | */ |
282 | public static function decodeChunkedData(string $data): string |
283 | { |
284 | $decoded = ''; |
285 | |
286 | while($data != '') { |
287 | $lfPos = strpos($data, "\012"); |
288 | |
289 | if ($lfPos === false) { |
290 | $decoded .= $data; |
291 | break; |
292 | } |
293 | |
294 | $chunkHex = trim(substr($data, 0, $lfPos)); |
295 | $scPos = strpos($chunkHex, ';'); |
296 | |
297 | if ($scPos !== false) { |
298 | $chunkHex = substr($chunkHex, 0, $scPos); |
299 | } |
300 | |
301 | if ($chunkHex == '') { |
302 | $decoded .= substr($data, 0, $lfPos); |
303 | $data = substr($data, $lfPos + 1); |
304 | continue; |
305 | } |
306 | |
307 | $chunkLength = hexdec($chunkHex); |
308 | |
309 | if ($chunkLength) { |
310 | $decoded .= substr($data, $lfPos + 1, $chunkLength); |
311 | $data = substr($data, $lfPos + 2 + $chunkLength); |
312 | } else { |
313 | $data = ''; |
314 | } |
315 | } |
316 | |
317 | return $decoded; |
318 | } |
319 | |
320 | } |