Passed
Push — master ( feb128...404971 )
by Gaetano
09:34
created

Http::setLogger()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 3
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace PhpXmlRpc\Helper;
4
5
use PhpXmlRpc\Exception\HttpException;
6
use PhpXmlRpc\PhpXmlRpc;
7
8
/**
9
 *
10
 */
11
class Http
12
{
13
    protected static $logger;
14
15
    public function getLogger()
16
    {
17
        if (self::$logger === null) {
18
            self::$logger = Logger::instance();
19
        }
20
        return self::$logger;
21
    }
22
23
    /**
24
     * @param $logger
25
     * @return void
26
     */
27
    public static function setLogger($logger)
28
    {
29
        self::$logger = $logger;
30
    }
31
32
    /**
33
     * Decode a string that is encoded with "chunked" transfer encoding as defined in rfc2068 par. 19.4.6.
34
     * Code shamelessly stolen from nusoap library by Dietrich Ayala.
35
     * @internal this function will become protected in the future
36
     *
37
     * @param string $buffer the string to be decoded
38
     * @return string
39
     */
40
    public static function decodeChunked($buffer)
41
    {
42
        // length := 0
43
        $length = 0;
44
        $new = '';
45
46
        // read chunk-size, chunk-extension (if any) and crlf
47
        // get the position of the linebreak
48
        $chunkEnd = strpos($buffer, "\r\n") + 2;
49
        $temp = substr($buffer, 0, $chunkEnd);
50
        $chunkSize = hexdec(trim($temp));
51
        $chunkStart = $chunkEnd;
52
        while ($chunkSize > 0) {
53
            $chunkEnd = strpos($buffer, "\r\n", $chunkStart + $chunkSize);
0 ignored issues
show
Bug introduced by
$chunkStart + $chunkSize of type double is incompatible with the type integer expected by parameter $offset of strpos(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

53
            $chunkEnd = strpos($buffer, "\r\n", /** @scrutinizer ignore-type */ $chunkStart + $chunkSize);
Loading history...
54
55
            // just in case we got a broken connection
56
            if ($chunkEnd == false) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $chunkEnd of type integer to the boolean false. If you are specifically checking for 0, consider using something more explicit like === 0 instead.
Loading history...
57
                $chunk = substr($buffer, $chunkStart);
58
                // append chunk-data to entity-body
59
                $new .= $chunk;
60
                $length += strlen($chunk);
61
                break;
62
            }
63
64
            // read chunk-data and crlf
65
            $chunk = substr($buffer, $chunkStart, $chunkEnd - $chunkStart);
66
            // append chunk-data to entity-body
67
            $new .= $chunk;
68
            // length := length + chunk-size
69
            $length += strlen($chunk);
70
            // read chunk-size and crlf
71
            $chunkStart = $chunkEnd + 2;
72
73 699
            $chunkEnd = strpos($buffer, "\r\n", $chunkStart) + 2;
74
            if ($chunkEnd == false) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $chunkEnd of type integer to the boolean false. If you are specifically checking for 0, consider using something more explicit like === 0 instead.
Loading history...
75 699
                break; // just in case we got a broken connection
76
            }
77
            $temp = substr($buffer, $chunkStart, $chunkEnd - $chunkStart);
78 699
            $chunkSize = hexdec(trim($temp));
79
            $chunkStart = $chunkEnd;
80
        }
81 32
82 32
        return $new;
83 32
    }
84
85
    /**
86
     * Parses HTTP an http response's headers and separates them from the body.
87
     *
88
     * @param string $data the http response, headers and body. It will be stripped of headers
89
     * @param bool $headersProcessed when true, we assume that response inflating and dechunking has been already carried out
90
     * @param int $debug when != 0, logs to screen messages detailing info about the parsed data
91
     * @return array with keys 'headers', 'cookies', 'raw_data' and 'status_code'
92
     * @throws HttpException
93 32
     *
94
     * @todo if $debug is 0, we could avoid populating 'raw_data' and 'headers' in the returned value - even better, have
95
     *       2 debug levels
96 32
     */
97
    public function parseResponseHeaders(&$data, $headersProcessed = false, $debug = 0)
98
    {
99
        $httpResponse = array('raw_data' => $data, 'headers'=> array(), 'cookies' => array(), 'status_code' => null);
100
101
        // Support "web-proxy-tunnelling" connections for https through proxies
102
        if (preg_match('/^HTTP\/1\.[0-1] 200 Connection established/', $data)) {
103
            // Look for CR/LF or simple LF as line separator (even though it is not valid http)
104 699
            $pos = strpos($data, "\r\n\r\n");
105 1
            if ($pos || is_int($pos)) {
0 ignored issues
show
introduced by
The condition is_int($pos) is always true.
Loading history...
106
                $bd = $pos + 4;
107
            } else {
108 1
                $pos = strpos($data, "\n\n");
109
                if ($pos || is_int($pos)) {
110
                    $bd = $pos + 2;
111
                } else {
112
                    // No separation between response headers and body: fault?
113 1
                    $bd = 0;
114
                }
115
            }
116
            if ($bd) {
117
                // this filters out all http headers from proxy. maybe we could take them into account, too?
118 699
                $data = substr($data, $bd);
119 32
            } else {
120 32
                $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': HTTPS via proxy error, tunnel connection possibly failed');
121
                throw new HttpException(PhpXmlRpc::$xmlrpcstr['http_error'] . ' (HTTPS via proxy error, tunnel connection possibly failed)', PhpXmlRpc::$xmlrpcerr['http_error']);
122
            }
123
        }
124 699
125 699
        // Strip HTTP 1.1 100 Continue header if present
126 699
        while (preg_match('/^HTTP\/1\.1 1[0-9]{2} /', $data)) {
127
            $pos = strpos($data, 'HTTP', 12);
128
            // server sent a Continue header without any (valid) content following...
129 699
            // give the client a chance to know it
130 3
            if (!$pos && !is_int($pos)) {
131 3
                /// @todo this construct works fine in php 3, 4 and 5 - 8; would it not be enough to have !== false now ?
132 3
133
                break;
134
            }
135
            $data = substr($data, $pos);
136
        }
137 696
138 696
        // When using Curl to query servers using Digest Auth, we get back a double set of http headers.
139 695
        // Same when following redirects
140
        // We strip out the 1st...
141 1
        /// @todo we should let the caller know that there was a redirect involved
142 1
        if ($headersProcessed && preg_match('/^HTTP\/[0-9](?:\.[0-9])? (?:401|30[1278]) /', $data)) {
143 1
            if (preg_match('/(\r?\n){2}HTTP\/[0-9](?:\.[0-9])? 200 /', $data)) {
144
                $data = preg_replace('/^HTTP\/[0-9](?:\.[0-9])? (?:401|30[1278]) .+?(?:\r?\n){2}(HTTP\/[0-9.]+ 200 )/s', '$1', $data, 1);
145
            }
146
        }
147
148
        if (preg_match('/^HTTP\/([0-9](?:\.[0-9])?) ([0-9]{3}) /', $data, $matches)) {
149
            $httpResponse['protocol_version'] = $matches[1];
150
            $httpResponse['status_code'] = $matches[2];
151
        }
152 696
153
        if ($httpResponse['status_code'] !== '200') {
154 696
            $errstr = substr($data, 0, strpos($data, "\n") - 1);
155
            $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': HTTP error, got response: ' . $errstr);
156 696
            throw new HttpException(PhpXmlRpc::$xmlrpcstr['http_error'] . ' (' . $errstr . ')', PhpXmlRpc::$xmlrpcerr['http_error'], null, $httpResponse['status_code']);
157 696
        }
158 696
159
        // be tolerant to usage of \n instead of \r\n to separate headers and data (even though it is not valid http)
160
        $pos = strpos($data, "\r\n\r\n");
161
        if ($pos || is_int($pos)) {
0 ignored issues
show
introduced by
The condition is_int($pos) is always true.
Loading history...
162
            $bd = $pos + 4;
163
        } else {
164 696
            $pos = strpos($data, "\n\n");
165 22
            if ($pos || is_int($pos)) {
166
                $bd = $pos + 2;
167
            } else {
168
                // No separation between response headers and body: fault?
169
                // we could take some action here instead of going on...
170 22
                $bd = 0;
171
            }
172 22
        }
173
174
        // be tolerant to line endings, and extra empty lines
175 22
        $ar = preg_split("/\r?\n/", trim(substr($data, 0, $pos)));
176 22
177
        foreach ($ar as $line) {
178 22
            // take care of multi-line headers and cookies
179
            $arr = explode(':', $line, 2);
180
            if (count($arr) > 1) {
181
                $headerName = strtolower(trim($arr[0]));
182
                /// @todo some other headers (the ones that allow a CSV list of values) do allow many values to be
183 22
                ///       passed using multiple header lines.
184 22
                ///       We should add content to $xmlrpc->_xh['headers'][$headerName] instead of replacing it for those...
185 22
                /// @todo should we drop support for rfc2965 (set-cookie2) cookies? It has been obsoleted since 2011
186 22
                if ($headerName == 'set-cookie' || $headerName == 'set-cookie2') {
187 22
                    if ($headerName == 'set-cookie2') {
188
                        // version 2 cookies:
189 22
                        // there could be many cookies on one line, comma separated
190 22
                        $cookies = explode(',', $arr[1]);
191 22
                    } else {
192 22
                        $cookies = array($arr[1]);
193
                    }
194 22
                    foreach ($cookies as $cookie) {
195 22
                        // glue together all received cookies, using a comma to separate them (same as php does with getallheaders())
196
                        if (isset($httpResponse['headers'][$headerName])) {
197
                            $httpResponse['headers'][$headerName] .= ', ' . trim($cookie);
198
                        } else {
199
                            $httpResponse['headers'][$headerName] = trim($cookie);
200
                        }
201 696
                        // parse cookie attributes, in case user wants to correctly honour them
202
                        // feature creep: only allow rfc-compliant cookie attributes?
203
                        // @todo support for server sending multiple time cookie with same name, but using different PATHs
204
                        $cookie = explode(';', $cookie);
205 1
                        foreach ($cookie as $pos => $val) {
206
                            $val = explode('=', $val, 2);
207
                            $tag = trim($val[0]);
208
                            $val = isset($val[1]) ? trim($val[1]) : '';
209 696
                            /// @todo with version 1 cookies, we should strip leading and trailing " chars
210
                            if ($pos == 0) {
211 696
                                $cookiename = $tag;
212 3
                                $httpResponse['cookies'][$tag] = array();
213 3
                                $httpResponse['cookies'][$cookiename]['value'] = urldecode($val);
214 3
                            } else {
215
                                if ($tag != 'value') {
216 3
                                    $httpResponse['cookies'][$cookiename][$tag] = $val;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $cookiename does not seem to be defined for all execution paths leading up to this point.
Loading history...
217
                                }
218
                            }
219 3
                        }
220
                    }
221
                } else {
222
                    $httpResponse['headers'][$headerName] = trim($arr[1]);
223
                }
224 696
            } elseif (isset($headerName)) {
225
                /// @todo version1 cookies might span multiple lines, thus breaking the parsing above
226
                $httpResponse['headers'][$headerName] .= ' ' . trim($line);
227 374
            }
228
        }
229
230
        $data = substr($data, $bd);
231
232
        if ($debug && count($httpResponse['headers'])) {
233
            $msg = '';
234
            foreach ($httpResponse['headers'] as $header => $value) {
235
                $msg .= "HEADER: $header: $value\n";
236 374
            }
237 71
            foreach ($httpResponse['cookies'] as $header => $value) {
238 71
                $msg .= "COOKIE: $header={$value['value']}\n";
239
            }
240 71
            $this->getLogger()->debugMessage($msg);
241 71
        }
242 32
243 32
        // if CURL was used for the call, http headers have been processed, and dechunking + reinflating have been carried out
244 32
        if (!$headersProcessed) {
245
246 39
            // Decode chunked encoding sent by http 1.1 servers
247 39
            if (isset($httpResponse['headers']['transfer-encoding']) && $httpResponse['headers']['transfer-encoding'] == 'chunked') {
248 39
                if (!$data = static::decodeChunked($data)) {
249 35
                    $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': errors occurred when trying to rebuild the chunked data received from server');
250
                    throw new HttpException(PhpXmlRpc::$xmlrpcstr['dechunk_fail'], PhpXmlRpc::$xmlrpcerr['dechunk_fail'], null, $httpResponse['status_code']);
251
                }
252
            }
253 64
254
            // Decode gzip-compressed stuff
255
            // code shamelessly inspired from nusoap library by Dietrich Ayala
256
            if (isset($httpResponse['headers']['content-encoding'])) {
257
                $httpResponse['headers']['content-encoding'] = str_replace('x-', '', $httpResponse['headers']['content-encoding']);
258
                if ($httpResponse['headers']['content-encoding'] == 'deflate' || $httpResponse['headers']['content-encoding'] == 'gzip') {
259
                    // if decoding works, use it. else assume data wasn't gzencoded
260
                    if (function_exists('gzinflate')) {
261
                        if ($httpResponse['headers']['content-encoding'] == 'deflate' && $degzdata = @gzuncompress($data)) {
262
                            $data = $degzdata;
263 696
                            if ($debug) {
264
                                $this->getLogger()->debugMessage("---INFLATED RESPONSE---[" . strlen($data) . " chars]---\n$data\n---END---");
265
                            }
266
                        } elseif ($httpResponse['headers']['content-encoding'] == 'gzip' && $degzdata = @gzinflate(substr($data, 10))) {
267
                            $data = $degzdata;
268
                            if ($debug) {
269
                                $this->getLogger()->debugMessage("---INFLATED RESPONSE---[" . strlen($data) . " chars]---\n$data\n---END---");
270
                            }
271
                        } else {
272
                            $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': errors occurred when trying to decode the deflated data received from server');
273
                            throw new HttpException(PhpXmlRpc::$xmlrpcstr['decompress_fail'], PhpXmlRpc::$xmlrpcerr['decompress_fail'], null, $httpResponse['status_code']);
274
                        }
275
                    } else {
276
                        $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': the server sent deflated data. Your php install must have the Zlib extension compiled in to support this.');
277
                        throw new HttpException(PhpXmlRpc::$xmlrpcstr['cannot_decompress'], PhpXmlRpc::$xmlrpcerr['cannot_decompress'], null, $httpResponse['status_code']);
278
                    }
279
                }
280
            }
281
        } // end of 'if needed, de-chunk, re-inflate response'
282
283
        return $httpResponse;
284
    }
285
286
    /**
287
     * Parses one of the http headers which can have a list of values with quality param.
288
     * @see https://www.rfc-editor.org/rfc/rfc7231#section-5.3.1
289
     *
290
     * @param string $header
291
     * @return string[]
292
     */
293
    public function parseAcceptHeader($header)
294
    {
295
        $accepted = array();
296
        foreach(explode(',', $header) as $c) {
297
            if (preg_match('/^([^;]+); *q=([0-9.]+)/', $c, $matches)) {
298
                $c = $matches[1];
299
                $w = $matches[2];
300
            } else {
301
                $c = preg_replace('/;.*/', '', $c);
302
                $w = 1;
303
            }
304
            $accepted[(trim($c))] = $w;
305
        }
306
        arsort($accepted);
307
        return array_keys($accepted);
308
    }
309
}
310