Completed
Push — master ( 081f49...6be6c0 )
by Gaetano
03:29
created

Http   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 251
Duplicated Lines 9.16 %

Coupling/Cohesion

Components 0
Dependencies 1

Test Coverage

Coverage 32.41%

Importance

Changes 0
Metric Value
dl 23
loc 251
ccs 47
cts 145
cp 0.3241
rs 7.92
c 0
b 0
f 0
wmc 51
lcom 0
cbo 1

2 Methods

Rating   Name   Duplication   Size   Complexity  
A decodeChunked() 0 44 4
F parseResponseHeaders() 23 186 47

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Http often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Http, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace PhpXmlRpc\Helper;
4
5
use PhpXmlRpc\PhpXmlRpc;
6
7
class Http
8
{
9
    /**
10
     * Decode a string that is encoded with "chunked" transfer encoding as defined in rfc2068 par. 19.4.6
11
     * Code shamelessly stolen from nusoap library by Dietrich Ayala.
12
     *
13
     * @param string $buffer the string to be decoded
14
     *
15
     * @return string
16
     */
17
    public static function decodeChunked($buffer)
18
    {
19
        // length := 0
20
        $length = 0;
21
        $new = '';
22
23
        // read chunk-size, chunk-extension (if any) and crlf
24
        // get the position of the linebreak
25
        $chunkEnd = strpos($buffer, "\r\n") + 2;
26
        $temp = substr($buffer, 0, $chunkEnd);
27
        $chunkSize = hexdec(trim($temp));
28
        $chunkStart = $chunkEnd;
29
        while ($chunkSize > 0) {
30
            $chunkEnd = strpos($buffer, "\r\n", $chunkStart + $chunkSize);
31
32
            // just in case we got a broken connection
33
            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...
34
                $chunk = substr($buffer, $chunkStart);
35
                // append chunk-data to entity-body
36
                $new .= $chunk;
37
                $length += strlen($chunk);
38
                break;
39
            }
40
41
            // read chunk-data and crlf
42
            $chunk = substr($buffer, $chunkStart, $chunkEnd - $chunkStart);
43
            // append chunk-data to entity-body
44
            $new .= $chunk;
45
            // length := length + chunk-size
46
            $length += strlen($chunk);
47
            // read chunk-size and crlf
48
            $chunkStart = $chunkEnd + 2;
49
50
            $chunkEnd = strpos($buffer, "\r\n", $chunkStart) + 2;
51
            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...
52
                break; //just in case we got a broken connection
53
            }
54
            $temp = substr($buffer, $chunkStart, $chunkEnd - $chunkStart);
55
            $chunkSize = hexdec(trim($temp));
56
            $chunkStart = $chunkEnd;
57
        }
58
59
        return $new;
60
    }
61
62
    /**
63
     * Parses HTTP an http response headers and separates them from the body.
64
     *
65
     * @param string $data the http response,headers and body. It will be stripped of headers
66
     * @param bool $headersProcessed when true, we assume that response inflating and dechunking has been already carried out
67
     *
68
     * @return array with keys 'headers' and 'cookies'
69
     * @throws \Exception
70
     */
71 498
    public function parseResponseHeaders(&$data, $headersProcessed = false, $debug=0)
72
    {
73 498
        $httpResponse = array('raw_data' => $data, 'headers'=> array(), 'cookies' => array());
74
75
        // Support "web-proxy-tunnelling" connections for https through proxies
76 498
        if (preg_match('/^HTTP\/1\.[0-1] 200 Connection established/', $data)) {
77
            // Look for CR/LF or simple LF as line separator,
78
            // (even though it is not valid http)
79
            $pos = strpos($data, "\r\n\r\n");
80 View Code Duplication
            if ($pos || is_int($pos)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
81
                $bd = $pos + 4;
82
            } else {
83
                $pos = strpos($data, "\n\n");
84
                if ($pos || is_int($pos)) {
85
                    $bd = $pos + 2;
86
                } else {
87
                    // No separation between response headers and body: fault?
88
                    $bd = 0;
89
                }
90
            }
91
            if ($bd) {
92
                // this filters out all http headers from proxy.
93
                // maybe we could take them into account, too?
94
                $data = substr($data, $bd);
95
            } else {
96
                error_log('XML-RPC: ' . __METHOD__ . ': HTTPS via proxy error, tunnel connection possibly failed');
97
                throw new \Exception(PhpXmlRpc::$xmlrpcstr['http_error'] . ' (HTTPS via proxy error, tunnel connection possibly failed)', PhpXmlRpc::$xmlrpcerr['http_error']);
98
            }
99
        }
100
101
        // Strip HTTP 1.1 100 Continue header if present
102 498
        while (preg_match('/^HTTP\/1\.1 1[0-9]{2} /', $data)) {
103 1
            $pos = strpos($data, 'HTTP', 12);
104
            // server sent a Continue header without any (valid) content following...
105
            // give the client a chance to know it
106 1
            if (!$pos && !is_int($pos)) {
107
                // works fine in php 3, 4 and 5
108
109
                break;
110
            }
111 1
            $data = substr($data, $pos);
112 1
        }
113
114
        // When using Curl to query servers using Digest Auth, we get back a double set of http headers.
115
        // We strip out the 1st...
116 498
        if ($headersProcessed && preg_match('/^HTTP\/[0-9.]+ 401 /', $data)) {
117
            if (preg_match('/(\r?\n){2}HTTP\/[0-9.]+ 200 /', $data)) {
118
                $data = preg_replace('/^HTTP\/[0-9.]+ 401 .+?(?:\r?\n){2}(HTTP\/[0-9.]+ 200 )/s', '$1', $data, 1);
119
            }
120
        }
121
122 498
        if (!preg_match('/^HTTP\/[0-9.]+ 200 /', $data)) {
123 495
            $errstr = substr($data, 0, strpos($data, "\n") - 1);
124 495
            error_log('XML-RPC: ' . __METHOD__ . ': HTTP error, got response: ' . $errstr);
125 495
            throw new \Exception(PhpXmlRpc::$xmlrpcstr['http_error'] . ' (' . $errstr . ')', PhpXmlRpc::$xmlrpcerr['http_error']);
126
        }
127
128
        // be tolerant to usage of \n instead of \r\n to separate headers and data
129
        // (even though it is not valid http)
130 3
        $pos = strpos($data, "\r\n\r\n");
131 3 View Code Duplication
        if ($pos || is_int($pos)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
132 2
            $bd = $pos + 4;
133 2
        } else {
134 1
            $pos = strpos($data, "\n\n");
135 1
            if ($pos || is_int($pos)) {
136 1
                $bd = $pos + 2;
137 1
            } else {
138
                // No separation between response headers and body: fault?
139
                // we could take some action here instead of going on...
140
                $bd = 0;
141
            }
142
        }
143
144
        // be tolerant to line endings, and extra empty lines
145 3
        $ar = preg_split("/\r?\n/", trim(substr($data, 0, $pos)));
146
147 3
        foreach($ar as $line) {
148
            // take care of multi-line headers and cookies
149 3
            $arr = explode(':', $line, 2);
150 3
            if (count($arr) > 1) {
151 3
                $headerName = strtolower(trim($arr[0]));
152
                /// @todo some other headers (the ones that allow a CSV list of values)
153
                /// do allow many values to be passed using multiple header lines.
154
                /// We should add content to $xmlrpc->_xh['headers'][$headerName]
155
                /// instead of replacing it for those...
156 3
                if ($headerName == 'set-cookie' || $headerName == 'set-cookie2') {
157
                    if ($headerName == 'set-cookie2') {
158
                        // version 2 cookies:
159
                        // there could be many cookies on one line, comma separated
160
                        $cookies = explode(',', $arr[1]);
161
                    } else {
162
                        $cookies = array($arr[1]);
163
                    }
164
                    foreach ($cookies as $cookie) {
165
                        // glue together all received cookies, using a comma to separate them
166
                        // (same as php does with getallheaders())
167
                        if (isset($httpResponse['headers'][$headerName])) {
168
                            $httpResponse['headers'][$headerName] .= ', ' . trim($cookie);
169
                        } else {
170
                            $httpResponse['headers'][$headerName] = trim($cookie);
171
                        }
172
                        // parse cookie attributes, in case user wants to correctly honour them
173
                        // feature creep: only allow rfc-compliant cookie attributes?
174
                        // @todo support for server sending multiple time cookie with same name, but using different PATHs
175
                        $cookie = explode(';', $cookie);
176
                        foreach ($cookie as $pos => $val) {
177
                            $val = explode('=', $val, 2);
178
                            $tag = trim($val[0]);
179
                            $val = trim(@$val[1]);
180
                            /// @todo with version 1 cookies, we should strip leading and trailing " chars
181
                            if ($pos == 0) {
182
                                $cookiename = $tag;
183
                                $httpResponse['cookies'][$tag] = array();
184
                                $httpResponse['cookies'][$cookiename]['value'] = urldecode($val);
185
                            } else {
186
                                if ($tag != 'value') {
187
                                    $httpResponse['cookies'][$cookiename][$tag] = $val;
0 ignored issues
show
Bug introduced by
The variable $cookiename does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
188
                                }
189
                            }
190
                        }
191
                    }
192
                } else {
193 3
                    $httpResponse['headers'][$headerName] = trim($arr[1]);
194
                }
195 3
            } elseif (isset($headerName)) {
196
                /// @todo version1 cookies might span multiple lines, thus breaking the parsing above
197 1
                $httpResponse['headers'][$headerName] .= ' ' . trim($line);
198 1
            }
199 3
        }
200
201 3
        $data = substr($data, $bd);
202
203 3
        if ($debug && count($httpResponse['headers'])) {
204 3
            $msg = '';
205 3
            foreach ($httpResponse['headers'] as $header => $value) {
206 3
                $msg .= "HEADER: $header: $value\n";
207 3
            }
208 3
            foreach ($httpResponse['cookies'] as $header => $value) {
209
                $msg .= "COOKIE: $header={$value['value']}\n";
210 3
            }
211 3
            Logger::instance()->debugMessage($msg);
212 3
        }
213
214
        // if CURL was used for the call, http headers have been processed,
215
        // and dechunking + reinflating have been carried out
216 3
        if (!$headersProcessed) {
217
218
            // Decode chunked encoding sent by http 1.1 servers
219 3
            if (isset($httpResponse['headers']['transfer-encoding']) && $httpResponse['headers']['transfer-encoding'] == 'chunked') {
220
                if (!$data = Http::decodeChunked($data)) {
221
                    error_log('XML-RPC: ' . __METHOD__ . ': errors occurred when trying to rebuild the chunked data received from server');
222
                    throw new \Exception(PhpXmlRpc::$xmlrpcstr['dechunk_fail'], PhpXmlRpc::$xmlrpcerr['dechunk_fail']);
223
                }
224
            }
225
226
            // Decode gzip-compressed stuff
227
            // code shamelessly inspired from nusoap library by Dietrich Ayala
228 3
            if (isset($httpResponse['headers']['content-encoding'])) {
229
                $httpResponse['headers']['content-encoding'] = str_replace('x-', '', $httpResponse['headers']['content-encoding']);
230
                if ($httpResponse['headers']['content-encoding'] == 'deflate' || $httpResponse['headers']['content-encoding'] == 'gzip') {
231
                    // if decoding works, use it. else assume data wasn't gzencoded
232
                    if (function_exists('gzinflate')) {
233
                        if ($httpResponse['headers']['content-encoding'] == 'deflate' && $degzdata = @gzuncompress($data)) {
234
                            $data = $degzdata;
235
                            if ($debug) {
236
                                Logger::instance()->debugMessage("---INFLATED RESPONSE---[" . strlen($data) . " chars]---\n$data\n---END---");
237
                            }
238
                        } elseif ($httpResponse['headers']['content-encoding'] == 'gzip' && $degzdata = @gzinflate(substr($data, 10))) {
239
                            $data = $degzdata;
240
                            if ($debug) {
241
                                Logger::instance()->debugMessage("---INFLATED RESPONSE---[" . strlen($data) . " chars]---\n$data\n---END---");
242
                            }
243
                        } else {
244
                            error_log('XML-RPC: ' . __METHOD__ . ': errors occurred when trying to decode the deflated data received from server');
245
                            throw new \Exception(PhpXmlRpc::$xmlrpcstr['decompress_fail'], PhpXmlRpc::$xmlrpcerr['decompress_fail']);
246
                        }
247
                    } else {
248
                        error_log('XML-RPC: ' . __METHOD__ . ': the server sent deflated data. Your php install must have the Zlib extension compiled in to support this.');
249
                        throw new \Exception(PhpXmlRpc::$xmlrpcstr['cannot_decompress'], PhpXmlRpc::$xmlrpcerr['cannot_decompress']);
250
                    }
251
                }
252
            }
253 3
        } // end of 'if needed, de-chunk, re-inflate response'
254
255 3
        return $httpResponse;
256
    }
257
}
258