Completed
Push — master ( 85e358...cf3219 )
by Rakesh
04:24 queued 10s
created

Zend_Http_Client_Adapter_Socket   F

Complexity

Total Complexity 88

Size/Duplication

Total Lines 494
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 204
c 1
b 0
f 1
dl 0
loc 494
rs 2
wmc 88

12 Methods

Rating   Name   Duplication   Size   Complexity  
A setOutputStream() 0 4 1
F read() 0 169 39
A setConfig() 0 14 4
A close() 0 5 2
A _checkSocketReadTimeout() 0 11 3
A getStreamContext() 0 7 2
C write() 0 48 12
A setStreamContext() 0 17 4
D connect() 0 55 16
A __destruct() 0 4 3
A getConfig() 0 3 1
A __construct() 0 2 1

How to fix   Complexity   

Complex Class

Complex classes like Zend_Http_Client_Adapter_Socket 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.

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 Zend_Http_Client_Adapter_Socket, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Zend Framework
5
 *
6
 * LICENSE
7
 *
8
 * This source file is subject to the new BSD license that is bundled
9
 * with this package in the file LICENSE.txt.
10
 * It is also available through the world-wide-web at this URL:
11
 * http://framework.zend.com/license/new-bsd
12
 * If you did not receive a copy of the license and are unable to
13
 * obtain it through the world-wide-web, please send an email
14
 * to [email protected] so we can send you a copy immediately.
15
 *
16
 * @category   Zend
17
 * @package    Zend_Http
18
 * @subpackage Client_Adapter
19
 * @version    $Id$
20
 * @copyright  Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com)
21
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
22
 */
23
24
/**
25
 * @see Zend_Uri_Http
26
 */
27
require_once 'Zend/Uri/Http.php';
28
/**
29
 * @see Zend_Http_Client_Adapter_Interface
30
 */
31
require_once 'Zend/Http/Client/Adapter/Interface.php';
32
/**
33
 * @see Zend_Http_Client_Adapter_Stream
34
 */
35
require_once 'Zend/Http/Client/Adapter/Stream.php';
36
37
/**
38
 * A sockets based (stream_socket_client) adapter class for Zend_Http_Client. Can be used
39
 * on almost every PHP environment, and does not require any special extensions.
40
 *
41
 * @category   Zend
42
 * @package    Zend_Http
43
 * @subpackage Client_Adapter
44
 * @copyright  Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com)
45
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
46
 */
47
class Zend_Http_Client_Adapter_Socket implements Zend_Http_Client_Adapter_Interface, Zend_Http_Client_Adapter_Stream
48
{
49
    /**
50
     * The socket for server connection
51
     *
52
     * @var resource|null
53
     */
54
    protected $socket = null;
55
56
    /**
57
     * What host/port are we connected to?
58
     *
59
     * @var array
60
     */
61
    protected $connected_to = array(null, null);
62
63
    /**
64
     * Stream for storing output
65
     *
66
     * @var resource
67
     */
68
    protected $out_stream = null;
69
70
    /**
71
     * Parameters array
72
     *
73
     * @var array
74
     */
75
    protected $config = array(
76
        'persistent'    => false,
77
        'ssltransport'  => 'ssl',
78
        'sslcert'       => null,
79
        'sslpassphrase' => null,
80
        'sslusecontext' => false
81
    );
82
83
    /**
84
     * Request method - will be set by write() and might be used by read()
85
     *
86
     * @var string
87
     */
88
    protected $method = null;
89
90
    /**
91
     * Stream context
92
     *
93
     * @var resource
94
     */
95
    protected $_context = null;
96
97
    /**
98
     * Adapter constructor, currently empty. Config is set using setConfig()
99
     *
100
     */
101
    public function __construct()
102
    {
103
    }
104
105
    /**
106
     * Set the configuration array for the adapter
107
     *
108
     * @param Zend_Config | array $config
109
     */
110
    public function setConfig($config = array())
111
    {
112
        if ($config instanceof Zend_Config) {
0 ignored issues
show
Bug introduced by
The type Zend_Config was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
113
            $config = $config->toArray();
114
115
        } elseif (! is_array($config)) {
0 ignored issues
show
introduced by
The condition is_array($config) is always true.
Loading history...
116
            require_once 'Zend/Http/Client/Adapter/Exception.php';
117
            throw new Zend_Http_Client_Adapter_Exception(
118
                'Array or Zend_Config object expected, got ' . gettype($config)
119
            );
120
        }
121
122
        foreach ($config as $k => $v) {
123
            $this->config[strtolower($k)] = $v;
124
        }
125
    }
126
127
    /**
128
      * Retrieve the array of all configuration options
129
      *
130
      * @return array
131
      */
132
     public function getConfig()
133
     {
134
         return $this->config;
135
     }
136
137
     /**
138
     * Set the stream context for the TCP connection to the server
139
     *
140
     * Can accept either a pre-existing stream context resource, or an array
141
     * of stream options, similar to the options array passed to the
142
     * stream_context_create() PHP function. In such case a new stream context
143
     * will be created using the passed options.
144
     *
145
     * @since  Zend Framework 1.9
146
     *
147
     * @param  mixed $context Stream context or array of context options
148
     * @return Zend_Http_Client_Adapter_Socket
149
     */
150
    public function setStreamContext($context)
151
    {
152
        if (is_resource($context) && get_resource_type($context) == 'stream-context') {
153
            $this->_context = $context;
154
155
        } elseif (is_array($context)) {
156
            $this->_context = stream_context_create($context);
157
158
        } else {
159
            // Invalid parameter
160
            require_once 'Zend/Http/Client/Adapter/Exception.php';
161
            throw new Zend_Http_Client_Adapter_Exception(
162
                "Expecting either a stream context resource or array, got " . gettype($context)
163
            );
164
        }
165
166
        return $this;
167
    }
168
169
    /**
170
     * Get the stream context for the TCP connection to the server.
171
     *
172
     * If no stream context is set, will create a default one.
173
     *
174
     * @return resource
175
     */
176
    public function getStreamContext()
177
    {
178
        if (! $this->_context) {
179
            $this->_context = stream_context_create();
180
        }
181
182
        return $this->_context;
183
    }
184
185
    /**
186
     * Connect to the remote server
187
     *
188
     * @param string  $host
189
     * @param int     $port
190
     * @param boolean $secure
191
     */
192
    public function connect($host, $port = 80, $secure = false)
193
    {
194
        // If the URI should be accessed via SSL, prepend the Hostname with ssl://
195
        $host = ($secure ? $this->config['ssltransport'] : 'tcp') . '://' . $host;
196
197
        // If we are connected to the wrong host, disconnect first
198
        if (($this->connected_to[0] != $host || $this->connected_to[1] != $port)) {
199
            if (is_resource($this->socket)) $this->close();
200
        }
201
202
        // Now, if we are not connected, connect
203
        if (! is_resource($this->socket) || ! $this->config['keepalive']) {
204
            $context = $this->getStreamContext();
205
            if ($secure || $this->config['sslusecontext']) {
206
                if ($this->config['sslcert'] !== null) {
207
                    if (! stream_context_set_option($context, 'ssl', 'local_cert',
208
                                                    $this->config['sslcert'])) {
209
                        require_once 'Zend/Http/Client/Adapter/Exception.php';
210
                        throw new Zend_Http_Client_Adapter_Exception('Unable to set sslcert option');
211
                    }
212
                }
213
                if ($this->config['sslpassphrase'] !== null) {
214
                    if (! stream_context_set_option($context, 'ssl', 'passphrase',
215
                                                    $this->config['sslpassphrase'])) {
216
                        require_once 'Zend/Http/Client/Adapter/Exception.php';
217
                        throw new Zend_Http_Client_Adapter_Exception('Unable to set sslpassphrase option');
218
                    }
219
                }
220
            }
221
222
            $flags = STREAM_CLIENT_CONNECT;
223
            if ($this->config['persistent']) $flags |= STREAM_CLIENT_PERSISTENT;
224
225
            $this->socket = @stream_socket_client($host . ':' . $port,
0 ignored issues
show
Documentation Bug introduced by
It seems like @stream_socket_client($h...ut'], $flags, $context) can also be of type false. However, the property $socket is declared as type null|resource. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
226
                                                  $errno,
227
                                                  $errstr,
228
                                                  (int) $this->config['timeout'],
229
                                                  $flags,
230
                                                  $context);
231
232
            if (! $this->socket) {
233
                $this->close();
234
                require_once 'Zend/Http/Client/Adapter/Exception.php';
235
                throw new Zend_Http_Client_Adapter_Exception(
236
                    'Unable to Connect to ' . $host . ':' . $port . '. Error #' . $errno . ': ' . $errstr);
237
            }
238
239
            // Set the stream timeout
240
            if (! stream_set_timeout($this->socket, (int) $this->config['timeout'])) {
241
                require_once 'Zend/Http/Client/Adapter/Exception.php';
242
                throw new Zend_Http_Client_Adapter_Exception('Unable to set the connection timeout');
243
            }
244
245
            // Update connected_to
246
            $this->connected_to = array($host, $port);
247
        }
248
    }
249
250
    /**
251
     * Send request to the remote server
252
     *
253
     * @param string        $method
254
     * @param Zend_Uri_Http $uri
255
     * @param string        $http_ver
256
     * @param array         $headers
257
     * @param string        $body
258
     * @return string Request as string
259
     */
260
    public function write($method, $uri, $http_ver = '1.1', $headers = array(), $body = '')
261
    {
262
        // Make sure we're properly connected
263
        if (! $this->socket) {
264
            require_once 'Zend/Http/Client/Adapter/Exception.php';
265
            throw new Zend_Http_Client_Adapter_Exception('Trying to write but we are not connected');
266
        }
267
268
        $host = $uri->getHost();
269
        $host = (strtolower($uri->getScheme()) == 'https' ? $this->config['ssltransport'] : 'tcp') . '://' . $host;
0 ignored issues
show
Bug introduced by
It seems like $uri->getScheme() can also be of type false; however, parameter $str of strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

269
        $host = (strtolower(/** @scrutinizer ignore-type */ $uri->getScheme()) == 'https' ? $this->config['ssltransport'] : 'tcp') . '://' . $host;
Loading history...
270
        if ($this->connected_to[0] != $host || $this->connected_to[1] != $uri->getPort()) {
271
            require_once 'Zend/Http/Client/Adapter/Exception.php';
272
            throw new Zend_Http_Client_Adapter_Exception('Trying to write but we are connected to the wrong host');
273
        }
274
275
        // Save request method for later
276
        $this->method = $method;
277
278
        // Build request headers
279
        $path = $uri->getPath();
280
        if ($uri->getQuery()) $path .= '?' . $uri->getQuery();
281
        $request = "{$method} {$path} HTTP/{$http_ver}\r\n";
282
        foreach ($headers as $k => $v) {
283
            if (is_string($k)) $v = ucfirst($k) . ": $v";
284
            $request .= "$v\r\n";
285
        }
286
287
        if(is_resource($body)) {
0 ignored issues
show
introduced by
The condition is_resource($body) is always false.
Loading history...
288
            $request .= "\r\n";
289
        } else {
290
            // Add the request body
291
            $request .= "\r\n" . $body;
292
        }
293
294
        // Send the request
295
        if (! @fwrite($this->socket, $request)) {
296
            require_once 'Zend/Http/Client/Adapter/Exception.php';
297
            throw new Zend_Http_Client_Adapter_Exception('Error writing request to server');
298
        }
299
300
        if(is_resource($body)) {
0 ignored issues
show
introduced by
The condition is_resource($body) is always false.
Loading history...
301
            if(stream_copy_to_stream($body, $this->socket) == 0) {
302
                require_once 'Zend/Http/Client/Adapter/Exception.php';
303
                throw new Zend_Http_Client_Adapter_Exception('Error writing request to server');
304
            }
305
        }
306
307
        return $request;
308
    }
309
310
    /**
311
     * Read response from server
312
     *
313
     * @return string
314
     */
315
    public function read()
316
    {
317
        // First, read headers only
318
        $response = '';
319
        $gotStatus = false;
320
321
        while (($line = @fgets($this->socket)) !== false) {
322
            $gotStatus = $gotStatus || (strpos($line, 'HTTP') !== false);
323
            if ($gotStatus) {
324
                $response .= $line;
325
                if (rtrim($line) === '') break;
326
            }
327
        }
328
329
        $this->_checkSocketReadTimeout();
330
331
        $statusCode = Zend_Http_Response::extractCode($response);
332
333
        // Handle 100 and 101 responses internally by restarting the read again
334
        if ($statusCode == 100 || $statusCode == 101) return $this->read();
335
336
        // Check headers to see what kind of connection / transfer encoding we have
337
        $headers = Zend_Http_Response::extractHeaders($response);
338
339
        /**
340
         * Responses to HEAD requests and 204 or 304 responses are not expected
341
         * to have a body - stop reading here
342
         */
343
        if ($statusCode == 304 || $statusCode == 204 ||
344
            $this->method == Zend_Http_Client::HEAD) {
345
346
            // Close the connection if requested to do so by the server
347
            if (isset($headers['connection']) && $headers['connection'] == 'close') {
348
                $this->close();
349
            }
350
            return $response;
351
        }
352
353
        // If we got a 'transfer-encoding: chunked' header
354
        if (isset($headers['transfer-encoding'])) {
355
356
            if (strtolower($headers['transfer-encoding']) == 'chunked') {
357
358
                do {
359
                    $line  = @fgets($this->socket);
360
                    $this->_checkSocketReadTimeout();
361
362
                    $chunk = $line;
363
364
                    // Figure out the next chunk size
365
                    $chunksize = trim($line);
366
                    if (! ctype_xdigit($chunksize)) {
367
                        $this->close();
368
                        require_once 'Zend/Http/Client/Adapter/Exception.php';
369
                        throw new Zend_Http_Client_Adapter_Exception('Invalid chunk size "' .
370
                            $chunksize . '" unable to read chunked body');
371
                    }
372
373
                    // Convert the hexadecimal value to plain integer
374
                    $chunksize = hexdec($chunksize);
375
376
                    // Read next chunk
377
                    $read_to = ftell($this->socket) + $chunksize;
378
379
                    do {
380
                        $current_pos = ftell($this->socket);
381
                        if ($current_pos >= $read_to) break;
382
383
                        if($this->out_stream) {
384
                            if(stream_copy_to_stream($this->socket, $this->out_stream, $read_to - $current_pos) == 0) {
0 ignored issues
show
Bug introduced by
$read_to - $current_pos of type double is incompatible with the type integer expected by parameter $maxlength of stream_copy_to_stream(). ( Ignorable by Annotation )

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

384
                            if(stream_copy_to_stream($this->socket, $this->out_stream, /** @scrutinizer ignore-type */ $read_to - $current_pos) == 0) {
Loading history...
385
                              $this->_checkSocketReadTimeout();
386
                              break;
387
                             }
388
                        } else {
389
                            $line = @fread($this->socket, $read_to - $current_pos);
0 ignored issues
show
Bug introduced by
$read_to - $current_pos of type double is incompatible with the type integer expected by parameter $length of fread(). ( Ignorable by Annotation )

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

389
                            $line = @fread($this->socket, /** @scrutinizer ignore-type */ $read_to - $current_pos);
Loading history...
390
                            if ($line === false || strlen($line) === 0) {
391
                                $this->_checkSocketReadTimeout();
392
                                break;
393
                            }
394
                                    $chunk .= $line;
395
                        }
396
                    } while (! feof($this->socket));
397
398
                    $chunk .= @fgets($this->socket);
399
                    $this->_checkSocketReadTimeout();
400
401
                    if(!$this->out_stream) {
402
                        $response .= $chunk;
403
                    }
404
                } while ($chunksize > 0);
405
            } else {
406
                $this->close();
407
        require_once 'Zend/Http/Client/Adapter/Exception.php';
408
                throw new Zend_Http_Client_Adapter_Exception('Cannot handle "' .
409
                    $headers['transfer-encoding'] . '" transfer encoding');
410
            }
411
412
            // We automatically decode chunked-messages when writing to a stream
413
            // this means we have to disallow the Zend_Http_Response to do it again
414
            if ($this->out_stream) {
415
                $response = str_ireplace("Transfer-Encoding: chunked\r\n", '', $response);
416
            }
417
        // Else, if we got the content-length header, read this number of bytes
418
        } elseif (isset($headers['content-length'])) {
419
420
            // If we got more than one Content-Length header (see ZF-9404) use
421
            // the last value sent
422
            if (is_array($headers['content-length'])) {
423
                $contentLength = $headers['content-length'][count($headers['content-length']) - 1];
424
            } else {
425
                $contentLength = $headers['content-length'];
426
            }
427
428
            $current_pos = ftell($this->socket);
429
            $chunk = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $chunk is dead and can be removed.
Loading history...
430
431
            for ($read_to = $current_pos + $contentLength;
432
                 $read_to > $current_pos;
433
                 $current_pos = ftell($this->socket)) {
434
435
                 if($this->out_stream) {
436
                     if(@stream_copy_to_stream($this->socket, $this->out_stream, $read_to - $current_pos) == 0) {
437
                          $this->_checkSocketReadTimeout();
438
                          break;
439
                     }
440
                 } else {
441
                    $chunk = @fread($this->socket, $read_to - $current_pos);
442
                    if ($chunk === false || strlen($chunk) === 0) {
443
                        $this->_checkSocketReadTimeout();
444
                        break;
445
                    }
446
447
                    $response .= $chunk;
448
                }
449
450
                // Break if the connection ended prematurely
451
                if (feof($this->socket)) break;
452
            }
453
454
        // Fallback: just read the response until EOF
455
        } else {
456
457
            do {
458
                if($this->out_stream) {
459
                    if(@stream_copy_to_stream($this->socket, $this->out_stream) == 0) {
460
                          $this->_checkSocketReadTimeout();
461
                          break;
462
                     }
463
                }  else {
464
                    $buff = @fread($this->socket, 8192);
465
                    if ($buff === false || strlen($buff) === 0) {
466
                        $this->_checkSocketReadTimeout();
467
                        break;
468
                    } else {
469
                        $response .= $buff;
470
                    }
471
                }
472
473
            } while (feof($this->socket) === false);
474
475
            $this->close();
476
        }
477
478
        // Close the connection if requested to do so by the server
479
        if (isset($headers['connection']) && $headers['connection'] == 'close') {
480
            $this->close();
481
        }
482
483
        return $response;
484
    }
485
486
    /**
487
     * Close the connection to the server
488
     *
489
     */
490
    public function close()
491
    {
492
        if (is_resource($this->socket)) @fclose($this->socket);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for fclose(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

492
        if (is_resource($this->socket)) /** @scrutinizer ignore-unhandled */ @fclose($this->socket);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
493
        $this->socket = null;
494
        $this->connected_to = array(null, null);
495
    }
496
497
    /**
498
     * Check if the socket has timed out - if so close connection and throw
499
     * an exception
500
     *
501
     * @throws Zend_Http_Client_Adapter_Exception with READ_TIMEOUT code
502
     */
503
    protected function _checkSocketReadTimeout()
504
    {
505
        if ($this->socket) {
506
            $info = stream_get_meta_data($this->socket);
507
            $timedout = $info['timed_out'];
508
            if ($timedout) {
509
                $this->close();
510
                require_once 'Zend/Http/Client/Adapter/Exception.php';
511
                throw new Zend_Http_Client_Adapter_Exception(
512
                    "Read timed out after {$this->config['timeout']} seconds",
513
                    Zend_Http_Client_Adapter_Exception::READ_TIMEOUT
514
                );
515
            }
516
        }
517
    }
518
519
    /**
520
     * Set output stream for the response
521
     *
522
     * @param resource $stream
523
     * @return Zend_Http_Client_Adapter_Socket
524
     */
525
    public function setOutputStream($stream)
526
    {
527
        $this->out_stream = $stream;
528
        return $this;
529
    }
530
531
    /**
532
     * Destructor: make sure the socket is disconnected
533
     *
534
     * If we are in persistent TCP mode, will not close the connection
535
     *
536
     */
537
    public function __destruct()
538
    {
539
        if (! $this->config['persistent']) {
540
            if ($this->socket) $this->close();
541
        }
542
    }
543
}
544