Issues (16)

src/Surfer/Message/Request.php (2 issues)

1
<?php
2
3
/**
4
 * @file Request.php
5
 * @brief This file contains the Request class.
6
 * @details
7
 * @author Filippo F. Fadda
8
 */
9
10
11
namespace Surfer\Message;
12
13
14
use ToolBag\Helper\ArrayHelper;
15
16
17
/**
18
 * @brief This class represents an HTTP request. Since CouchDB is a RESTful server, we need make requests through an
19
 * HTTP client. That's the purpose of this class: emulate an HTTP request.
20
 * @nosubgrouping
21
 */
22
final class Request extends Message {
23
24
  /** @name Request Header Fields */
25
  //!@{
26
27
  /**
28
   * @brief Used to tell the server what media types are okay to send.
29
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
30
   */
31
  const ACCEPT_HF = "Accept";
32
33
  /**
34
   * @brief Used to tell the server what charsets are okay to send.
35
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.2
36
   */
37
  const ACCEPT_CHARSET_HF = "Accept-Charset";
38
39
  /**
40
   * @brief Used to tell the server what encodings are okay to send.
41
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3
42
   */
43
  const ACCEPT_ENCODING_HF = "Accept-Encoding";
44
45
  /**
46
   * @brief Used the server which languages are okay to send.
47
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
48
   */
49
  const ACCEPT_LANGUAGE_HF = "Accept-Language";
50
51
  /**
52
   * @brief Contains the data the client is supplying to the server to authenticate itself.
53
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.8
54
   */
55
  const AUTHORIZATION_HF = "Authorization";
56
57
  /**
58
   * @brief Allows a client to list server behaviors that it requires for a request.
59
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.20
60
   */
61
  const EXPECT_HF = "Expect";
62
63
  /**
64
   * @brief The From request-header field, if given, SHOULD contain an Internet e-mail address for the human user who
65
   * controls the requesting user agent.
66
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.22
67
   */
68
  const FROM_HF = "From";
69
70
  /**
71
   * @brief Hostname and port of the server to which the request is being sent.
72
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.23
73
   */
74
  const HOST_HF = "Host";
75
76
  /**
77
   * @brief The If-Match request-header field is used with a method to make it conditional. A client that has one or
78
   * more entities previously obtained from the resource can verify that one of those entities is current by including
79
   * a list of their associated entity tags in the If-Match header field.
80
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.24
81
   */
82
  const IF_MATCH_HF = "If-Match";
83
84
  /**
85
   * @brief Restricts the request unless the resource has been modified since the specified date.
86
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.25
87
   */
88
  const IF_MODIFIED_SINCE_HF = "If-Modified-Since";
89
90
  /**
91
   * @brief The If-None-Match request-header field is used with a method to make it conditional. A client that has one
92
   * or more entities previously obtained from the resource can verify that none of those entities is current by including
93
   * a list of their associated entity tags in the If-None-Match header field. The purpose of this feature is to allow
94
   * efficient updates of cached information with a minimum amount of transaction overhead.
95
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
96
   */
97
  const IF_NONE_MATCH_HF = "If-None_Match";
98
99
  /**
100
   * @brief If a client has a partial copy of an entity in its cache, and wishes to have an up-to-date copy of the entire
101
   * entity in its cache, it could use the Range request-header with a conditional GET.
102
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.27
103
   */
104
  const IF_RANGE_HF = "If-Range";
105
106
  /**
107
   * @brief Restricts the request unless the resource has not been modified since the specified date.
108
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.28
109
   */
110
  const IF_UNMODIFIED_SINCE_HF = "If-Unmodified-Since-Header";
111
112
  /**
113
   * @brief The maximum number of times a request should be forwarded to another proxy or gateway on its way to the
114
   * origin server.
115
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.31
116
   */
117
  const MAX_FORWARDS_HF = "Max-Forwards";
118
119
  /**
120
   * @brief Same as AUTHORIZATION_HF, but used when authenticating with a proxy.
121
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.34
122
   */
123
  const PROXY_AUTHORIZATION_HF = "Proxy-Authorization";
124
125
  /**
126
   * @brief Request only part of an entity. Bytes are numbered from 0.
127
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
128
   */
129
  const RANGE_HF = "Range";
130
131
  /**
132
   * @brief The Referer request-header field allows the client to specify, for the server's benefit, the address (URI)
133
   * of the resource from which the Request-URI was obtained.
134
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.36
135
   */
136
  const REFERER_HF = "Referer";
137
138
  /**
139
   * @brief The TE request-header field indicates what extension transfer-codings it is willing to accept in the response
140
   * and whether or not it is willing to accept trailer fields in a chunked transfer-coding. Its value may consist of
141
   * the keyword "trailers" and/or a comma-separated list of extension transfer-coding names with optional accept
142
   * parameters.
143
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.39
144
   */
145
  const TE_HF = "TE";
146
147
  /**
148
   * @brief Ask the server to upgrade to another protocol.;
149
   * @see http://en.wikipedia.org/wiki/Upgrade_HF
150
   */
151
  const UPGRADE_HF = "Upgrade";
152
153
  /**
154
   * @brief User agent info.
155
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43
156
   */
157
  const USER_AGENT_HF = "User-Agent";
158
159
  /**
160
   * @brief Used by clients to pass a token to the server.
161
   * @see http://en.wikipedia.org/wiki/HTTP_Cookie
162
   */
163
  const COOKIE_HF = "Cookie";
164
165
  /**
166
   * @brief Mainly used to identify Ajax requests. Most JavaScript frameworks send this header with value of XMLHttpRequest.
167
   */
168
  const X_REQUESTED_WITH_HF = "X-Requested-With";
169
170
  /**
171
   * @brief Requests a web application to disable their tracking of a user. Note that, as of yet, this is largely ignored
172
   * by web applications. It does however open the door to future legislation requiring web applications to comply with
173
   * a user's request to not be tracked. Mozilla implements the DNT header with a similar purpose.
174
   * @see http://en.wikipedia.org/wiki/X-Do-Not-Track
175
   */
176
  const X_DO_NOT_TRACK_HF = "X-Do-Not-Track";
177
178
  /**
179
   * @brief Requests a web application to disable their tracking of a user.
180
   * @details This is Mozilla's version of the X-Do-Not-Track header (since Firefox 4.0 Beta 11). Safari and IE9 also
181
   * have support for this header.[11] On March 7, 2011, a draft proposal was submitted to IETF.
182
   */
183
  const DNT_HF = "DNT";
184
185
  /**
186
   * @brief A de facto standard for identifying the originating IP address of a client connecting to a web server through
187
   * an HTTP proxy or load balancer.
188
   * @see http://en.wikipedia.org/wiki/X-Forwarded-For
189
   */
190
  const X_FORWARDED_FOR_HF = "X-Forwarded-For";
191
192
  const DESTINATION_HF = "Destination";  //!< This header field is not supported by HTTP 1.1. It's a special header field used by CouchDB.
193
  const X_COUCHDB_WWW_AUTHENTICATE_HF = "X-CouchDB-WWW-Authenticate";  //!< This header field is not supported by HTTP 1.1. It's a special method header field by CouchDB.
194
  const X_COUCHDB_FULL_COMMIT_HF = "X-Couch-Full-Commit";  //!< This header field is not supported by HTTP 1.1. It's a special header field used by CouchDB.
195
196
  //!@}
197
198
  // Stores the header fields supported by a Request.
199
  protected static $supportedHeaderFields = array( // Cannot use [] syntax otherwise Doxygen generates a warning.
200
    self::ACCEPT_HF => NULL,
201
    self::ACCEPT_CHARSET_HF => NULL,
202
    self::ACCEPT_ENCODING_HF => NULL,
203
    self::ACCEPT_LANGUAGE_HF => NULL,
204
    self::AUTHORIZATION_HF => NULL,
205
    self::EXPECT_HF => NULL,
206
    self::FROM_HF => NULL,
207
    self::HOST_HF => NULL,
208
    self::IF_MATCH_HF => NULL,
209
    self::IF_MODIFIED_SINCE_HF => NULL,
210
    self::IF_NONE_MATCH_HF => NULL,
211
    self::IF_RANGE_HF => NULL,
212
    self::IF_UNMODIFIED_SINCE_HF => NULL,
213
    self::MAX_FORWARDS_HF => NULL,
214
    self::PROXY_AUTHORIZATION_HF => NULL,
215
    self::RANGE_HF => NULL,
216
    self::REFERER_HF => NULL,
217
    self::TE_HF => NULL,
218
    self::UPGRADE_HF => NULL,
219
    self::USER_AGENT_HF => NULL,
220
    self::COOKIE_HF => NULL,
221
    self::X_REQUESTED_WITH_HF => NULL,
222
    self::X_DO_NOT_TRACK_HF => NULL,
223
    self::DNT_HF => NULL,
224
    self::X_FORWARDED_FOR_HF => NULL,
225
    self::DESTINATION_HF => NULL,
226
    self::X_COUCHDB_WWW_AUTHENTICATE_HF => NULL,
227
    self::X_COUCHDB_FULL_COMMIT_HF => NULL,
228
  );
229
230
  /** @name Request Methods */
231
  //!@{
232
  const GET_METHOD = "GET";
233
  const HEAD_METHOD = "HEAD";
234
  const POST_METHOD = "POST";
235
  const PUT_METHOD = "PUT";
236
  const DELETE_METHOD = "DELETE";
237
  const COPY_METHOD = "COPY"; //!< This method is not supported by HTTP 1.1. It's a special method used by CouchDB.
238
  //!@}
239
240
  // Stores the request methods supported by HTTP 1.1 protocol.
241
  private static $supportedMethods = array( // Cannot use [] syntax otherwise Doxygen generates a warning.
242
    self::GET_METHOD => NULL,
243
    self::HEAD_METHOD => NULL,
244
    self::POST_METHOD => NULL,
245
    self::PUT_METHOD => NULL,
246
    self::DELETE_METHOD => NULL,
247
    self::COPY_METHOD => NULL
248
  );
249
250
  // Used to know if the constructor has been already called.
251
  private static $initialized = FALSE;
252
253
  // Stores the request method.
254
  private $method;
255
256
  // Stores the request method.
257
  private $path;
258
259
  // Stores the request query's parameters.
260
  private $queryParams = [];
261
262
263
  /**
264
   * @brief Creates an instance of Request class.
265
   * @param string $method The HTTP method for the request.
266
   * @param string $path The absolute path of the request.
267
   * @param array $queryParams (optional) Associative array of query parameters.
268
   * @param array $headerFields (optional) Associative array of header fields.
269
   */
270
  public function __construct($method, $path, array $queryParams = NULL, array $headerFields = NULL) {
271
    parent::__construct();
272
273
    // We can avoid to call the following code every time a Request instance is created, testing a static property.
274
    // Because the static nature of self::$initialized, this code will be executed only one time, even multiple Request
275
    // instances are created.
276
    if (!self::$initialized) {
277
      self::$initialized = TRUE;
278
      self::$supportedHeaderFields += parent::$supportedHeaderFields;
279
    }
280
281
    $this->setMethod($method);
282
    $this->setPath($path);
283
284
    if (isset($queryParams))
285
      $this->setMultipleQueryParamsAtOnce($queryParams);
286
287
    if (isset($headerFields))
288
      $this->setMultipleHeaderFieldsAtOnce($headerFields);
289
  }
290
291
292
  /**
293
   * Returns a comprehensible representation of the HTTP Request to be used for debugging purpose.
294
   * @retval string
295
   */
296
  public function __toString() {
297
    $request = [
298
      $this->getMethod()." ".$this->getPath().$this->getQueryString(),
299
      $this->getHeaderAsString(),
300
      $this->getBody()
301
    ];
302
303
    return implode(PHP_EOL.PHP_EOL, $request);
304
  }
305
306
307
  /**
308
   * @brief Retrieves the HTTP method used by the current request.
309
   * @retval string
310
   */
311
  public function getMethod() {
312
    return $this->method;
313
  }
314
315
316
  /**
317
   * @brief Sets the HTTP method used by the current request.
318
   * @param string $method The HTTP method for the request. You should use one of the public constants, like GET_METHOD.
319
   */
320
  public function setMethod($method) {
321
    if (array_key_exists($method, self::$supportedMethods))
322
      $this->method = $method;
323
    else
324
      throw new \UnexpectedValueException("$method method not supported. Use addCustomMethod() to add an unsupported method.");
325
  }
326
327
328
  /**
329
   * @brief Adds a non standard HTTP method.
330
   * @param string $method The HTTP custom method.
331
   */
332
  public static function addCustomMethod($method) {
333
    if (array_key_exists($method, self::$supportedMethods))
334
      throw new \UnexpectedValueException("$method method is supported and already exists.");
335
    else
336
      self::$supportedMethods[] = $method;
337
  }
338
339
340
  /**
341
   * @brief Gets the absolute path for the current request.
342
   * @retval string
343
   */
344
  public function getPath() {
345
    return $this->path;
346
  }
347
348
349
  /**
350
   * @brief Sets the request absolute path.
351
   * @param string $path The absolute path of the request.
352
   */
353
  public function setPath($path) {
354
    if (is_string($path))
0 ignored issues
show
The condition is_string($path) is always true.
Loading history...
355
      $this->path = addslashes($path);
356
    else
357
      throw new \InvalidArgumentException("\$path must be a string.");
358
  }
359
360
361
  /**
362
   * @brief Used to set a query parameter. You can set many query parameters you want.
363
   * @param string $name Parameter name.
364
   * @param string $value Parameter value.
365
   */
366
  public function setQueryParam($name, $value) {
367
    if (preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/', $name))
368
      $this->queryParams[$name] = $value;
369
    else
370
      throw new \InvalidArgumentException("\$name must start with a letter or underscore, followed by any number of
371
          letters, numbers, or underscores.");
372
  }
373
374
375
  /**
376
   * @brief Used to set many parameters at once.
377
   * @param array $params An associative array of parameters.
378
   */
379
  public function setMultipleQueryParamsAtOnce(array $params) {
380
    if (ArrayHelper::isAssociative($params))
381
      foreach ($params as $name => $value)
382
        $this->setQueryParam($name, $value);
383
    else
384
      throw new \InvalidArgumentException("\$params must be an associative array.");
385
  }
386
387
388
  /**
389
   * @brief Generates URL-encoded query string.
390
   * @retval string
391
   */
392
  public function getQueryString() {
393
    if (empty($this->queryParams))
394
      return "";
395
    else
396
      // Encoding is based on RFC 3986.
397
      return "?".http_build_query($this->queryParams, NULL, "&", PHP_QUERY_RFC3986);
0 ignored issues
show
NULL of type null is incompatible with the type string expected by parameter $numeric_prefix of http_build_query(). ( Ignorable by Annotation )

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

397
      return "?".http_build_query($this->queryParams, /** @scrutinizer ignore-type */ NULL, "&", PHP_QUERY_RFC3986);
Loading history...
398
  }
399
400
401
  /**
402
   * @brief Parses the given query string and sets every single parameter contained into it.
403
   */
404
  public function setQueryString($value) {
405
    if (!empty($value)) {
406
      $query = ltrim($value, "?");
407
408
      $params = explode('&', $query);
409
410
      foreach ($params as $param) {
411
        @list($name, $value)  = explode('=', $param, 2);
412
        $this->setQueryParam($name, $value);
413
      }
414
    }
415
  }
416
417
418
  /**
419
   * @brief This helper forces request to use the Authorization Basic mode.
420
   * @param string $userName User name.
421
   * @param string $password Password.
422
   */
423
  public function setBasicAuth($userName, $password) {
424
    $this->setHeaderField(self::AUTHORIZATION_HF, "Basic ".base64_encode("$userName:$password"));
425
  }
426
427
428
  /**
429
   * @brief This helper forces request to use the Bearer authentication mode.
430
   * @param string $value The value of the bearer.
431
   */
432
  public function setBearerAuth($value) {
433
    $this->setHeaderField(self::AUTHORIZATION_HF, "Bearer ".(string)$value);
434
  }
435
436
437
  /**
438
   * @brief Returns a list of all supported methods.
439
   * @retval array An associative array
440
   */
441
  public function getSupportedMethods() {
442
    return self::$supportedMethods;
443
  }
444
445
}