Issues (16)

src/Surfer/Adapter/SocketAdapter.php (10 issues)

Labels
Severity
1
<?php
2
3
/**
4
 * @file SocketAdapter.php
5
 * @brief This file contains the SocketAdapter class.
6
 * @details
7
 * @author Filippo F. Fadda
8
 */
9
10
11
namespace Surfer\Adapter;
12
13
14
use Surfer\Message\Message;
15
use Surfer\Message\Request;
16
use Surfer\Message\Response;
17
use Surfer\Hook\IChunkHook;
18
19
20
/**
21
 * @brief An HTTP 1.1 client using raw sockets.
22
 * @details This client is using HTTP/1.1 version.\n
23
 * Encoding is made according RFC 3986, using rawurlencode().\n
24
 * It supports 100-continue, chunked responses, persistent connections, etc.
25
 * @nosubgrouping
26
 */
27
class SocketAdapter extends AbstractAdapter {
28
29
  //! HTTP protocol version.
30
  const HTTP_VERSION = "HTTP/1.1";
31
32
  //! Buffer dimension.
33
  const BUFFER_LENGTH = 8192;
34
35
  //! Maximum period to wait before the response is sent.
36
  const DEFAULT_TIMEOUT = 60000;
37
38
  protected static $defaultSocketTimeout;
39
40
  // Socket connection timeout in seconds, specified by a float.
41
  protected $timeout;
42
43
  // Socket handle.
44
  protected $handle;
45
46
47
  /**
48
   * @copydoc AbstractAdapter::__construct()
49
   * @param bool $persistent (optional) When `true` the client uses a persistent connection.
50
  */
51
  public function __construct($server = parent::DEFAULT_SERVER, $userName = "", $password = "", $persistent = TRUE) {
52
    $this->initialize();
53
54
    parent::__construct($server, $userName, $password);
55
56
    $this->timeout = static::$defaultSocketTimeout;
57
58
    // Establishes a connection within the server.
59
    if ($persistent)
60
      $this->handle = @pfsockopen($this->scheme.$this->host, $this->port, $errno, $errstr, $this->timeout);
61
    else
62
      $this->handle = @fsockopen($this->scheme.$this->host, $this->port, $errno, $errstr, $this->timeout);
63
64
    if (!is_resource($this->handle))
65
      throw new \ErrorException($errstr, $errno);
66
  }
67
68
69
  /**
70
   * @brief Closes the file pointer.
71
   */
72
  public function __destruct() {
73
    //@fclose($this->handle);
74
  }
75
76
77
  /**
78
   * @copydoc AbstractAdapter::initialize()
79
   */
80
  public function initialize() {
81
82
    if (!static::$initialized) {
83
      static::$initialized = TRUE;
84
85
      // If PHP is not properly recognizing the line endings when reading files either on or created by a Macintosh
86
      // computer, enabling the auto_detect_line_endings run-time configuration option may help resolve the problem.
87
      ini_set("auto_detect_line_endings", TRUE);
0 ignored issues
show
TRUE of type true is incompatible with the type string expected by parameter $value of ini_set(). ( Ignorable by Annotation )

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

87
      ini_set("auto_detect_line_endings", /** @scrutinizer ignore-type */ TRUE);
Loading history...
88
89
      // By default the default_socket_timeout php.ini setting is used.
90
      static::$defaultSocketTimeout = ini_get("default_socket_timeout");
91
    }
92
  }
93
94
95
  /**
96
   * @brief Writes the entire request over the socket.
97
   * @param[in] Request $request A request.
98
   */
99
  protected function writeRequest(Request $request) {
100
    $command = $request->getMethod()." ".$request->getPath().$request->getQueryString()." ".self::HTTP_VERSION;
101
102
    // Writes the request over the socket.
103
    fputs($this->handle, $command.Message::CRLF);
0 ignored issues
show
It seems like $this->handle can also be of type boolean; however, parameter $stream of fputs() does only seem to accept resource, 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

103
    fputs(/** @scrutinizer ignore-type */ $this->handle, $command.Message::CRLF);
Loading history...
104
    fputs($this->handle, $request->getHeaderAsString().Message::CRLF);
105
    fputs($this->handle, Message::CRLF);
106
    fputs($this->handle, $request->getBody());
107
    fputs($this->handle, Message::CRLF);
108
  }
109
110
111
  /**
112
   * @brief Reads the the status code and the header of the response.
113
   * @return string
114
   */
115
  protected function readResponseStatusCodeAndHeader() {
116
    $statusCodeAndHeader = "";
117
118
    while (!feof($this->handle)) {
0 ignored issues
show
It seems like $this->handle can also be of type boolean; however, parameter $stream of feof() does only seem to accept resource, 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

118
    while (!feof(/** @scrutinizer ignore-type */ $this->handle)) {
Loading history...
119
      // We use fgets() because it stops reading at first newline or buffer length, depends which one is reached first.
120
      $buffer = fgets($this->handle, self::BUFFER_LENGTH);
0 ignored issues
show
It seems like $this->handle can also be of type boolean; however, parameter $stream of fgets() does only seem to accept resource, 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

120
      $buffer = fgets(/** @scrutinizer ignore-type */ $this->handle, self::BUFFER_LENGTH);
Loading history...
121
122
      // Adds the buffer to the header.
123
      $statusCodeAndHeader .= $buffer;
124
125
      // The header is separated from the body by a newline, so we break when we read it.
126
      if ($buffer == Message::CRLF)
127
        break;
128
    }
129
130
    return $statusCodeAndHeader;
131
  }
132
133
  /**
134
   * @brief Reads the entity-body of a chunked response.
135
   * @see http://www.jmarshall.com/easy/http/#http1.1c2
136
   * @param IChunkHook $chunkHook (optional) The chunk's hook.
137
   * @return string
138
   */
139
  protected function readChunkedResponseBody($chunkHook) {
140
    $body = "";
141
142
    while (!feof($this->handle)) {
0 ignored issues
show
It seems like $this->handle can also be of type boolean; however, parameter $stream of feof() does only seem to accept resource, 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

142
    while (!feof(/** @scrutinizer ignore-type */ $this->handle)) {
Loading history...
143
      // Gets the line which has the length of this chunk.
144
      $line = fgets($this->handle, self::BUFFER_LENGTH);
0 ignored issues
show
It seems like $this->handle can also be of type boolean; however, parameter $stream of fgets() does only seem to accept resource, 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

144
      $line = fgets(/** @scrutinizer ignore-type */ $this->handle, self::BUFFER_LENGTH);
Loading history...
145
146
      // If it's only a newline, this normally means it's read the total amount of data requested minus the newline
147
      // continue to next loop to make sure we're done.
148
      if ($line == Message::CRLF)
149
        continue;
150
151
      // The length of the block is expressed in hexadecimal.
152
      $length = hexdec($line);
153
154
      if (!is_int($length))
155
        throw new \RuntimeException("The response doesn't seem chunk encoded.");
156
157
      // Zero is sent when at the end of the chunks or the end of the stream.
158
      if ($length < 1)
159
        break;
160
161
      // Reads the chunk.
162
      // When reading from network streams or pipes, such as those returned when reading remote files or from popen()
163
      // and proc_open(), reading will stop after a new packet is available. This means that we must collect the data
164
      // together in chunks. So, we can't pass to the fread() the entire length because it could return less data than
165
      // expected. We have to read, instead, the standard buffer length, and concatenate the read chunks.
166
      $buffer = "";
167
168
      while ($length > 0) {
169
        $size = min(self::BUFFER_LENGTH, $length);
170
        $data = fread($this->handle, $size);
0 ignored issues
show
It seems like $this->handle can also be of type boolean; however, parameter $stream of fread() does only seem to accept resource, 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

170
        $data = fread(/** @scrutinizer ignore-type */ $this->handle, $size);
Loading history...
171
172
        if (strlen($data) == 0)
173
          break; // EOF
174
175
        $buffer .= $data;
176
        $length -= strlen($data);
177
      }
178
179
      // If a function has been hooked, calls it, else just adds the buffer to the body.
180
      if (is_null($chunkHook))
181
        $body .= $buffer;
182
      else
183
        $chunkHook->process($buffer);
184
    }
185
186
    // A chunk response might have some footer, but CouchDB doesn't use them, so we simply ignore them.
187
    while (!feof($this->handle)) {
188
      // We use fgets() because it stops reading at first newline or buffer length, depends which one is reached first.
189
      $buffer = fgets($this->handle, self::BUFFER_LENGTH);
190
191
      // The chunk response ends with a newline, so we break when we read it.
192
      if ($buffer == Message::CRLF)
193
        break;
194
    }
195
196
    return $body;
197
  }
198
199
200
  /**
201
   * @brief Reads the entity-body of a standard response.
202
   * @param Response $response The response.
203
   * @return string
204
   */
205
  protected function readStandardResponseBody(Response $response) {
206
    $body = "";
207
208
    // Retrieves the body length from the header.
209
    $length = (int)$response->getHeaderFieldValue(Response::CONTENT_LENGTH_HF);
210
211
    // The response should have a body, if not we have finished.
212
    if ($length > 0) {
213
      $bytes = 0;
214
215
      while (!feof($this->handle)) {
0 ignored issues
show
It seems like $this->handle can also be of type boolean; however, parameter $stream of feof() does only seem to accept resource, 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

215
      while (!feof(/** @scrutinizer ignore-type */ $this->handle)) {
Loading history...
216
        $buffer = fgets($this->handle);
0 ignored issues
show
It seems like $this->handle can also be of type boolean; however, parameter $stream of fgets() does only seem to accept resource, 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

216
        $buffer = fgets(/** @scrutinizer ignore-type */ $this->handle);
Loading history...
217
        $body .= $buffer;
218
        $bytes += strlen($buffer);
219
220
        if ($bytes >= $length)
221
          break;
222
      }
223
    }
224
225
    return $body;
226
  }
227
228
229
  /**
230
   * @brief Reads the entity-body.
231
   * @param Response $response The response.
232
   * @param IChunkHook $chunkHook (optional) The chunk's hook.
233
   * @return string
234
   */
235
  protected function readResponseBody(Response $response, $chunkHook) {
236
    if ($response->getHeaderFieldValue(Response::TRANSFER_ENCODING_HF) == "chunked")
237
      return $this->readChunkedResponseBody($chunkHook);
238
    else
239
      return $this->readStandardResponseBody($response);
240
  }
241
242
243
  /**
244
   * @copydoc AbstractAdapter::setTimeout()
245
   */
246
  public function setTimeout($seconds) {
247
    return stream_set_timeout($this->handle, $seconds);
0 ignored issues
show
It seems like $this->handle can also be of type boolean; however, parameter $stream of stream_set_timeout() does only seem to accept resource, 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

247
    return stream_set_timeout(/** @scrutinizer ignore-type */ $this->handle, $seconds);
Loading history...
248
  }
249
250
251
  /**
252
   * @copydoc AbstractAdapter::send()
253
   */
254
  public function send(Request $request, IChunkHook $chunkHook = NULL) {
255
    $request->setHeaderField(Request::HOST_HF, $this->host.":".$this->port);
256
257
    if (!empty($this->userName))
258
      $request->setBasicAuth($this->userName, $this->password);
259
260
    // Sets the Content-Length header only when the given request has a message body.
261
    if ($request->hasBody())
262
      $request->setHeaderField(Message::CONTENT_LENGTH_HF, $request->getBodyLength());
263
264
    // Writes the request over the socket.
265
    $this->writeRequest($request);
266
267
    // Creates the Response object.
268
    $response = new Response($this->readResponseStatusCodeAndHeader());
269
270
    // The Content-Length entity-header field indicates the size of the entity-body, in decimal number of OCTETs, sent
271
    // to the recipient or, in the case of the HEAD method, the size of the entity-body that would have been sent had
272
    // the request been a GET.
273
    if ($request->getMethod() != Request::HEAD_METHOD) {
274
      // Assigns the body to the response, if any is present.
275
      $response->setBody($this->readResponseBody($response, $chunkHook));
276
    }
277
278
    return $response;
279
  }
280
281
}