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