Issues (16)

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

Severity
1
<?php
2
3
/**
4
 * @file Response.php
5
 * @brief This file contains the Response class.
6
 * @details
7
 * @author Filippo F. Fadda
8
 */
9
10
11
namespace Surfer\Message;
12
13
14
/**
15
 * @brief After receiving and interpreting a request message, a server responds with an HTTP response message. This class
16
 * represents a server response.
17
 * @nosubgrouping
18
 */
19
final class Response extends Message {
20
21
  /** @name Response Header Fields */
22
  //!@{
23
24
  /**
25
   * @brief The Accept-Ranges response-header field allows the server to indicate its acceptance of range requests for
26
   * a resource.
27
   * @see http://www.w3.org/cols/rfc2616/rfc2616-sec14.html#sec14.5
28
   */
29
  const ACCEPT_RANGES_HF = "Accept-Ranges";
30
31
  /**
32
   * @brief The Age response-header field conveys the sender's estimate of the amount of time since the response (or its
33
   * revalidation) was generated at the origin server. A cached response is "fresh" if its age does not exceed its
34
   * freshness lifetime.
35
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.6
36
   */
37
  const AGE_HF = "Age";
38
39
  /**
40
   * @brief In order to force the browser to show SaveAs dialog when clicking a hyperlink you have to include this
41
   * header field.
42
   */
43
  const CONTENT_DISPOSITION_HF = "Content-Disposition";
44
45
  /**
46
   * @brief An identifier for a specific version of a resource, often a message digest.
47
   * @attention The constant should be ETag, not Etag, see the below bug.
48
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19
49
   * @bug https://issues.apache.org/jira/browse/COUCHDB-3105
50
   */
51
  const ETAG_HF = "Etag";
52
53
  /**
54
   * @brief Used to express a typed relationship with another resource, where the relation type is defined by RFC 5988.
55
   * @see http://tools.ietf.org/html/rfc5988
56
   */
57
  const LINK_HF = "Link";
58
59
  /**
60
   * @brief Used in redirection for completion of the request or identification of a new resource, or when a new resource
61
   * has been created.
62
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.30
63
   */
64
  const LOCATION_HF = "Location";
65
66
  /**
67
   * @brief This header is supposed to set P3P policy, in the form of P3P:CP="your_compact_policy". However, P3P did not
68
   * take off, most browsers have never fully implemented it, a lot of websites set this header with fake policy text,
69
   * that was enough to fool browsers the existence of P3P policy and grant permissions for third party cookies.
70
   * @see http://en.wikipedia.org/wiki/P3P
71
   */
72
  const P3P_HF = "P3P";
73
74
  /**
75
   * @brief Request authentication to access the proxy.
76
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.33
77
   */
78
  const PROXY_AUTHENTICATE_HF = "Proxy-Authenticate";
79
80
  /**
81
   * @brief Used in redirection, or when a new resource has been created. This refresh redirects after 5 seconds. This is
82
   * a proprietary, non-standard header extension introduced by Netscape and supported by most web browsers.
83
   * @see http://en.wikipedia.org/wiki/HTTP_refresh
84
   */
85
  const REFRESH_HF = "Refresh";
86
87
  /**
88
   * @brief If an entity is temporarily unavailable, this instructs the client to try again after a specified period of time (seconds).
89
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.37
90
   */
91
  const RETRY_AFTER_HF = "Retry-After";
92
93
  /**
94
   * @brief This response-header specifies the server name.
95
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.38
96
   */
97
  const SERVER_HF = "Server";
98
99
  /**
100
   * @brief Sets an HTTP Cookie.
101
   * @see http://en.wikipedia.org/wiki/HTTP_cookie
102
   */
103
  const SET_COOKIE_HF = "Set-Cookie";
104
105
  /**
106
   * @brief A HSTS Policy informing the HTTP client how long to cache the HTTPS only policy and whether this applies to
107
   * subdomains.
108
   * @see http://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security
109
   */
110
  const STRICT_TRANSPORT_SECURITY_HF = "Strict-Transport-Security";
111
112
  /**
113
   * @brief Tells downstream proxies how to match future request headers to decide whether the cached response can be
114
   * used rather than requesting a fresh one from the origin server.
115
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.44
116
   */
117
  const VARY_HF = "Vary";
118
119
  /**
120
   * @brief Indicates the authentication scheme that should be used to access the requested entity.
121
   * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.47
122
   */
123
  const WWW_AUTHENTICATE_HF = "WWW-Authenticate";
124
125
  /**
126
   * @brief Clickjacking protection: "deny" - no rendering within a frame, "sameorigin" - no rendering if origin mismatch.
127
   * @see http://en.wikipedia.org/wiki/Clickjacking
128
   */
129
  const X_FRAME_OPTIONS_HF = "X-Frame-Options";
130
131
  /**
132
   * @brief Cross-site scripting (XSS) filter.
133
   * @see http://en.wikipedia.org/wiki/Cross-site_scripting
134
   */
135
  const X_XSS_PROTECTION_HF = "X-XSS-Protection";
136
137
  /**
138
   * @brief The only defined value, "nosniff", prevents Internet Explorer from MIME-sniffing a response away from the
139
   * declared content-type.
140
   */
141
  const X_CONTENT_TYPE_OPTIONS_HF = "X-Content-Type-Options";
142
143
  /**
144
   * @brief A de facto standard for identifying the originating protocol of an HTTP request, since a reverse proxy (load
145
   * balancer) may communicate with a web server using HTTP even if the request to the reverse proxy is HTTPS.
146
   */
147
  const X_FORWARDED_PROTO_HF = "X-Forwarded-Proto";
148
149
  /**
150
   * @brief Specifies the technology (e.g. ASP.NET, PHP, JBoss) supporting the web application (version details are often
151
   * in X-Runtime, X-Version, or X-AspNet-Version)
152
   */
153
  const X_POWERED_BY_HF = "X-Powered-By";
154
155
  /**
156
   * @brief Recommends the preferred rendering engine (often a backward-compatibility mode) to use to display the content.
157
   * Also used to activate Chrome Frame in Internet Explorer.
158
   * @see http://en.wikipedia.org/wiki/Chrome_Frame
159
   */
160
  const X_UA_COMPATIBLE_HF = "X-UA-Compatible";
161
162
  //!@}
163
164
  // Stores the header fields supported by a Response.
165
  protected static $supportedHeaderFields = array( // Cannot use [] syntax otherwise Doxygen generates a warning.
166
    self::ACCEPT_RANGES_HF => NULL,
167
    self::AGE_HF => NULL,
168
    self::CONTENT_DISPOSITION_HF => NULL,
169
    self::ETAG_HF => NULL,
170
    self::LINK_HF => NULL,
171
    self::LOCATION_HF => NULL,
172
    self::P3P_HF => NULL,
173
    self::PROXY_AUTHENTICATE_HF => NULL,
174
    self::REFRESH_HF => NULL,
175
    self::RETRY_AFTER_HF => NULL,
176
    self::SERVER_HF => NULL,
177
    self::SET_COOKIE_HF => NULL,
178
    self::STRICT_TRANSPORT_SECURITY_HF => NULL,
179
    self::VARY_HF => NULL,
180
    self::WWW_AUTHENTICATE_HF => NULL,
181
    self::X_FRAME_OPTIONS_HF => NULL,
182
    self::X_XSS_PROTECTION_HF => NULL,
183
    self::X_CONTENT_TYPE_OPTIONS_HF => NULL,
184
    self::X_FORWARDED_PROTO_HF => NULL,
185
    self::X_POWERED_BY_HF => NULL,
186
    self::X_UA_COMPATIBLE_HF => NULL
187
  );
188
189
  /** @name Informational Status Codes */
190
  //!@{
191
192
  /**
193
   * @brief Indicates that an initial part of the request was received and the client should continue.
194
   * @details After sending this, the server must respond after receiving the body request.
195
   */
196
  const CONTINUE_SC = 100;
197
198
  //!@}
199
200
  /** @name Success Status Codes
201
  //!@{
202
203
  /**
204
   * @brief Request is OK, entity body contains requested resource.
205
   */ 
206
  const OK_SC = 200;
207
208
  /**
209
   * @brief For requests that create server objects (e.g., PUT).
210
   * @details The entity body of the response should contain the various URLs for referencing the created resource,
211
   * with the Location header containing the most specific reference.
212
   */
213
  const CREATED_SC = 201;
214
215
  /**
216
   * @brief The request was accepted, but the server has not yet performed any action with it.
217
   * @details There are no guarantees that the server will complete the request; this just means that the request
218
   * looked valid when accepted.
219
   */
220
  const ACCEPTED_SC = 202;
221
222
  /**
223
   * @brief The response message contains headers and a status line, but no entity body.
224
   * @details Primarily used to update browsers without having them move to a new document.
225
   */
226
  const NO_CONTENT_SC = 204;
227
228
  /**
229
   * @brief A partial or range request was successful.
230
   * @details A 206 response must include a Content-Range, Date, and either ETag or Content-Location header.
231
   */
232
  const PARTIAL_CONTENT_SC = 206;
233
234
  //!@}
235
236
  /** @name Redirection Status Codes */
237
  //!@{
238
239
  /**
240
   * @brief Used when the requested URL has been moved.
241
   * @details The response should contain in the Location header the URL where the resource now resides.
242
   */
243
  const MOVED_PERMANENTLY_SC = 301;
244
245
  /**
246
   * @brief Like the 301 status code.
247
   * @details However, the client should use the URL given in the Location header to locate the resource temporarily.
248
   * Future requests should use the old URL.
249
   */
250
  const FOUND_SC = 302;
251
252
  /**
253
   * @brief Clients can make their requests conditional by the request headers they include.
254
   * @details If a client makes a conditional request, such as a GET if the resource has not been changed recently,
255
   * this code is used to indicate that the resource has not changed. Responses with this status code should not
256
   * contain an entity body.
257
   */
258
  const NOT_MODIFIED_SC = 304;
259
260
  //!@}
261
262
  /** @name Client Error Status Codes */
263
  //!@{
264
265
  /**
266
   * @brief Used to tell the client that it has sent a malformed request.
267
   */
268
  const BAD_REQUEST_SC = 400;
269
270
  /**
271
   * @brief Returned along with appropriate headers that ask the client to authenticate itself before it can gain access
272
   * to the resource.
273
   */
274
  const UNAUTHORIZED_SC = 401;
275
276
  /**
277
   * @brief Used to indicate that the request was refused by the server.
278
   * @details If the server wants to indicate why the request was denied, it can include an entity body describing the
279
   * reason. However, this code usually is used when the server does not want to reveal the reason for the refusal.
280
   */
281
  const FORBIDDEN_SC = 403;
282
283
  /**
284
   * @brief Used to indicate that the server cannot find the requested URL.
285
   * @details Often, an entity is included for the client application to display to the user.
286
   */
287
  const NOT_FOUND_SC = 404;
288
289
  /**
290
   * @brief Used when a request is made with a method that is not supported for the requested URL.
291
   * @details The Allow header should be included in the response to tell the client what methods are allowed on the
292
   * requested resource.
293
   */
294
  const METHOD_NOT_ALLOWED_SC = 405;
295
296
  /**
297
   * @brief Clients can specify parameters about what types of entities they are willing to accept.
298
   * @details This code is used when the server has no resource matching the URL that is acceptable for the client.
299
   * Often, servers include headers that allow the client to figure out why the request could not be satisfied.
300
   */
301
  const NOT_ACCEPTABLE_SC = 406;
302
303
  /**
304
   * @brief Used to indicate some conflict that the request may be causing on a resource.
305
   * @details Servers might send this code when they fear that a request could cause a conflict. The response should
306
   * contain a body describing the conflict.
307
   */
308
  const CONFLICT_SC = 409;
309
310
  /**
311
   * @brief Used if a client makes a conditional request and one of the conditions fails.
312
   * @details Conditional requests occur when a client includes an unexpected header.
313
   */
314
  const PRECONDITION_FAILED_SC = 412;
315
316
  /**
317
   * @brief Used when a client sends an entity body that is larger than the server can or wants to process.
318
   */
319
  const REQUEST_ENTITY_TOO_LARGE_SC = 413;
320
321
  /**
322
   * @brief Used when a client sends an entity of a content type that the server does not understand or support.
323
   */
324
  const UNSUPPORTED_MEDIA_TYPE_SC = 415;
325
326
  /**
327
   * @brief Used when the request message requested a range of a given resource and that range either was invalid or
328
   * could not be met.
329
   */
330
  const REQUESTED_RANGE_NOT_SATISFIABLE_SC = 416;
331
332
  /**
333
   * @brief Used when the request contained an expectation in the request header that the server could not satisfy.
334
   */
335
  const EXPECTATION_FAILED_SC = 417;
336
337
  //!@}
338
  
339
  /** @name Server Error Status Codes */
340
  //!@{
341
342
  /**
343
   * @brief Used when the server encounters an error that prevents it from servicing the request.
344
   */
345
  const INTERNAL_SERVER_ERROR_SC = 500;
346
347
  //!@}
348
349
  // Array of HTTP Status Codes
350
  private static $supportedStatusCodes = array( // Cannot use [] syntax otherwise Doxygen generates a warning.
351
      // Informational Status Codes
352
      self::CONTINUE_SC => "Continue",
353
      // Success Status Codes
354
      self::OK_SC => "OK",
355
      self::CREATED_SC => "Created",
356
      self::ACCEPTED_SC => "Accepted",
357
      self::NO_CONTENT_SC => "No Content",
358
      self::PARTIAL_CONTENT_SC => "Partial Content",
359
      // Redirection Status Codes
360
      self::MOVED_PERMANENTLY_SC => "Moved Permanently",
361
      self::FOUND_SC => "Found",
362
      self::NOT_MODIFIED_SC => "Not Modified",
363
      // Client Error Status Codes
364
      self::BAD_REQUEST_SC => "Bad Request",
365
      self::UNAUTHORIZED_SC => "Unauthorized",
366
      self::FORBIDDEN_SC => "Forbidden",
367
      self::NOT_FOUND_SC => "Not Found",
368
      self::METHOD_NOT_ALLOWED_SC => "Method Not Allowed",
369
      self::NOT_ACCEPTABLE_SC => "Not Acceptable",
370
      self::CONFLICT_SC => "Conflict",
371
      self::PRECONDITION_FAILED_SC => "Precondition Failed",
372
      self::REQUEST_ENTITY_TOO_LARGE_SC => "Request Entity Too Large",
373
      self::UNSUPPORTED_MEDIA_TYPE_SC => "Unsupported Media Type",
374
      self::REQUESTED_RANGE_NOT_SATISFIABLE_SC => "Requested Range Not Satisfiable",
375
      self::EXPECTATION_FAILED_SC => "Expectation Failed",
376
      // Server Error Status Codes
377
      self::INTERNAL_SERVER_ERROR_SC => "Internal Server Error",
378
  );
379
380
  private $statusCode;
381
382
  // Used to know if the constructor has been already called.
383
  private static $initialized = FALSE;
384
385
386
  /**
387
   * @brief Creates a new Response object.
388
   * @param string $message The complete Response string.
389
   */
390
  public function __construct($message) {
391
    parent::__construct();
392
393
    // We can avoid to call the following code every time a Response instance is created, testing a static property.
394
    // Because the static nature of self::$initialized, this code will be executed only one time, even multiple Response
395
    // instances are created.
396
    if (!self::$initialized) {
397
      self::$initialized = TRUE;
398
      self::$supportedHeaderFields += parent::$supportedHeaderFields;
399
    }
400
401
    $this->parseStatusCodeAndHeader($message);
402
  }
403
404
405
  /**
406
   * @brief Returns a comprehensible representation of the HTTP Response to be used for debugging purpose.
407
   * @retval string
408
   */
409
  public function __toString() {
410
    $response = [
411
      $this->getStatusCode()." ".$this->getSupportedStatusCodes()[$this->getStatusCode()],
412
      $this->getHeaderAsString(),
413
      $this->getBody()
414
    ];
415
416
    return implode(PHP_EOL.PHP_EOL, $response);
417
  }
418
419
420
  /**
421
   * @brief Parses the response's status code.
422
   * @param string $rawMessage The raw message.
423
   */
424
  protected function parseStatusCode($rawMessage) {
425
    $matches = [];
426
    if (preg_match('%HTTP/1\.[0-1] (\d\d\d) %', $rawMessage, $matches))
427
      $this->statusCode = $matches[1];
428
    else
429
      throw new \UnexpectedValueException("HTTP Status Code undefined.");
430
431
    if (!array_key_exists($this->statusCode, self::$supportedStatusCodes))
432
      throw new \UnexpectedValueException("HTTP Status Code unknown.");
433
  }
434
435
436
  /**
437
   * @brief Parses the response's header fields.
438
   * @param string $rawHeader The raw header.
439
   */
440
  protected function parseHeaderFields($rawHeader) {
441
    $fields = explode("\r\n", preg_replace('/\x0D\x0A[\x09\x20]+/', ' ', $rawHeader));
442
443
    foreach ($fields as $field) {
444
      if (preg_match('/([^:]+): (.+)/m', $field, $matches)) {
445
        // With the advent of PHP 5.5, the /e modifier is deprecated, so we use preg_replace_callback().
446
        $matches[1] = preg_replace_callback('/(?<=^|[\x09\x20\x2D])./',
447
            function($matches) {
448
              return strtoupper($matches[0]);
449
            },
450
            strtolower(trim($matches[1])));
451
452
        if (isset($this->header[$matches[1]]))
453
          $this->header[$matches[1]] = array($this->header[$matches[1]], $matches[2]);
454
        else
455
          $this->header[$matches[1]] = trim($matches[2]);
456
      }
457
    }
458
  }
459
460
461
  /**
462
   * @brief Parses the response's status code and header.
463
   * @param string $rawMessage The raw message.
464
   */
465
  protected function parseStatusCodeAndHeader($rawMessage) {
466
    if (!is_string($rawMessage))
0 ignored issues
show
The condition is_string($rawMessage) is always true.
Loading history...
467
      throw new \InvalidArgumentException("\$rawMessage must be a string.");
468
469
    if (empty($rawMessage))
470
      throw new \UnexpectedValueException("\$rawMessage is null.");
471
472
    $this->parseStatusCode($rawMessage);
473
474
    // In case server sends a "100 Continue" response, we must parse the message twice. This happens when a client uses
475
    // the "Expect: 100-continue" header field.
476
    // @see http://www.jmarshall.com/easy/http/#http1.1s5
477
    if ($this->statusCode == self::CONTINUE_SC) {
478
      $rawMessage = preg_split('/\r\n\r\n/', $rawMessage, 2)[1];
479
      $this->parseStatusCodeAndHeader($rawMessage);
480
    }
481
482
    $rawMessage = preg_split('/\r\n\r\n/', $rawMessage, 2);
483
484
    if (empty($rawMessage))
485
      throw new \RuntimeException("The server didn't return a valid Response for the Request.");
486
487
    // $rawMessage[0] contains header fields.
488
    $this->parseHeaderFields($rawMessage[0]);
489
490
    // $rawMessage[1] contains the entity-body.
491
    $this->body = $rawMessage[1];
492
  }
493
494
495
  /**
496
   * @brief Returns the HTTP Status Code for the current response.
497
   * @retval string
498
   */
499
  public function getStatusCode() {
500
    return $this->statusCode;
501
  }
502
503
504
  /**
505
   * @brief Sets the Response status code.
506
   * @param int $value The status code.
507
   */
508
  public function setStatusCode($value) {
509
    if (array_key_exists($value, self::$supportedStatusCodes)) {
510
      $this->statusCode = $value;
511
    }
512
    else
513
      throw new \UnexpectedValueException("Status Code $value is not supported.");
514
  }
515
516
517
  /**
518
   * @brief Returns a list of all supported status codes.
519
   * @retval array An associative array
520
   */
521
  public function getSupportedStatusCodes() {
522
    return self::$supportedStatusCodes;
523
  }
524
525
526
  /**
527
   * @brief Adds a non standard HTTP Status Code.
528
   * @param string $code The Status Code.
529
   * @param string $description A description for the Status Code.
530
   */
531
  public static function addCustomStatusCode($code, $description) {
532
    if (in_array($code, self::$supportedStatusCodes))
533
      throw new \UnexpectedValueException("Status Code $code is supported and already exists.");
534
    elseif (is_int($code) and $code > 0)
0 ignored issues
show
The condition is_int($code) is always false.
Loading history...
535
      self::$supportedStatusCodes[$code] = $description;
536
    else
537
      throw new \InvalidArgumentException("\$code must be a positive integer.");
538
  }
539
540
}