Response::meta()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 9

Duplication

Lines 12
Ratio 100 %

Code Coverage

Tests 6
CRAP Score 3.1406

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 3
nop 2
dl 12
loc 12
ccs 6
cts 8
cp 0.75
crap 3.1406
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Todd Burry <[email protected]>
4
 * @copyright 2009-2014 Vanilla Forums Inc.
5
 * @license MIT
6
 * @since 1.0
7
 */
8
9
namespace Garden;
10
11
use JsonSerializable;
12
13
/**
14
 * A class that contains the information in an http response.
15
 */
16
class Response implements JsonSerializable {
17
    /// Properties ///
18
19
    /**
20
     * An array of cookie sets. This array is in the form:
21
     *
22
     * ```
23
     * array (
24
     *     'name' => [args for setcookie()]
25
     * )
26
     * ```
27
     *
28
     * @var array An array of cookies sets.
29
     */
30
    protected $cookies = [];
31
32
    /**
33
     * An array of global cookie sets.
34
     *
35
     * This array is for code the queue up cookie changes before the response has been created.
36
     *
37
     * @var array An array of cookies.
38
     */
39
    protected static $globalCookies;
40
41
    /**
42
     * @var Response The current response.
43
     */
44
    protected static $current;
45
46
    /**
47
     * @var array An array of meta data that is not related to the response data.
48
     */
49
    protected $meta = [];
50
51
    /**
52
     * @var array An array of response data.
53
     */
54
    protected $data = [];
55
56
    /**
57
     * @var string The asset that should be rendered.
58
     */
59
    protected $contentAsset;
60
61
    /**
62
     * @var string The default cookie domain.
63
     */
64
    public $defaultCookieDomain;
65
66
    /**
67
     * @var string The default cookie path.
68
     */
69
    public $defaultCookiePath;
70
71
    /**
72
     * @var array An array of http headers.
73
     */
74
    protected $headers = array();
75
76
    /**
77
     * @var array An array of global http headers.
78
     */
79
    protected static $globalHeaders;
80
81
    /**
82
     * @var int HTTP status code
83
     */
84
    protected $status = 200;
85
86
    /**
87
     * @var array HTTP response codes and messages.
88
     */
89
    protected static $messages = array(
90
        // Informational 1xx
91
        100 => 'Continue',
92
        101 => 'Switching Protocols',
93
        // Successful 2xx
94
        200 => 'OK',
95
        201 => 'Created',
96
        202 => 'Accepted',
97
        203 => 'Non-Authoritative Information',
98
        204 => 'No Content',
99
        205 => 'Reset Content',
100
        206 => 'Partial Content',
101
        // Redirection 3xx
102
        300 => 'Multiple Choices',
103
        301 => 'Moved Permanently',
104
        302 => 'Found',
105
        303 => 'See Other',
106
        304 => 'Not Modified',
107
        305 => 'Use Proxy',
108
        306 => '(Unused)',
109
        307 => 'Temporary Redirect',
110
        // Client Error 4xx
111
        400 => 'Bad Request',
112
        401 => 'Unauthorized',
113
        402 => 'Payment Required',
114
        403 => 'Forbidden',
115
        404 => 'Not Found',
116
        405 => 'Method Not Allowed',
117
        406 => 'Not Acceptable',
118
        407 => 'Proxy Authentication Required',
119
        408 => 'Request Timeout',
120
        409 => 'Conflict',
121
        410 => 'Gone',
122
        411 => 'Length Required',
123
        412 => 'Precondition Failed',
124
        413 => 'Request Entity Too Large',
125
        414 => 'Request-URI Too Long',
126
        415 => 'Unsupported Media Type',
127
        416 => 'Requested Range Not Satisfiable',
128
        417 => 'Expectation Failed',
129
        418 => 'I\'m a teapot',
130
        422 => 'Unprocessable Entity',
131
        423 => 'Locked',
132
        // Server Error 5xx
133
        500 => 'Internal Server Error',
134
        501 => 'Not Implemented',
135
        502 => 'Bad Gateway',
136
        503 => 'Service Unavailable',
137
        504 => 'Gateway Timeout',
138
        505 => 'HTTP Version Not Supported'
139
    );
140
141
    /// Methods ///
142
143
    /**
144
     * Gets or sets the response that is currently being processed.
145
     *
146
     * @param Response|null $response Set a new response or pass null to get the current response.
147
     * @return Response Returns the current response.
148
     */
149
    public static function current(Response $response = null) {
150
        if ($response !== null) {
151
            self::$current = $response;
152
        } elseif (self::$current === null) {
153
            self::$current = new Response();
154
        }
155
156
        return self::$current;
157
    }
158
159
    /**
160
     * Create a Response from a variety of data.
161
     *
162
     * @param mixed $result The result to create the response from.
163
     * @return Response Returns a {@link Response} object.
164
     */
165 44
    public static function create($result) {
166 44
        if ($result instanceof Response) {
167
            return $result;
168 44
        } elseif ($result instanceof Exception\ResponseException) {
169
            /* @var Exception\ResponseException $result */
170
            return $result->getResponse();
171
        }
172
173 44
        $response = new Response();
174
175 44
        if ($result instanceof Exception\ClientException) {
176
            /* @var Exception\ClientException $cex */
177 23
            $cex = $result;
178 23
            $response->status($cex->getCode());
179 23
            $response->headers($cex->getHeaders());
180 23
            $response->data($cex->jsonSerialize());
181 44
        } elseif ($result instanceof \Exception) {
182
            /* @var \Exception $ex */
183
            $ex = $result;
184
            $response->status($ex->getCode());
185
            $response->data([
186
                'exception' => $ex->getMessage(),
187
                'code' => $ex->getCode()
188
            ]);
189 23
        } elseif (is_array($result)) {
190 23
            if (count($result) === 3 && isset($result[0], $result[1], $result[2])) {
191
                // This is a rack style response in the form [code, headers, body].
192
                $response->status($result[0]);
193
                $response->headers($result[1]);
194
                $response->data($result[2]);
195 23
            } elseif (array_key_exists('response', $result)) {
196 23
                $resultResponse = $result['response'];
197 23
                if (!$resultResponse) {
198 15
                    $response->data($result['body']);
199 15
                } else {
200
                    // This is a dispatched response.
201 8
                    $response = static::create($resultResponse);
202
                }
203
204
                // Set the rest of the result to the response context.
205 23
                unset($result['response']);
206 23
                $response->meta($result, true);
207 23
            } else {
208 8
                $response->data($result);
209
            }
210 23
        } else {
211
            $response->status(422);
212
            $response->data([
213
                'exception' => "Unknown result type for response.",
214
                'code' => $response->status()
215
            ]);
216
        }
217 44
        return $response;
218
    }
219
220
    /**
221
     * Gets or sets the content type.
222
     *
223
     * @param string|null $value The new content type or null to get the current content type.
224
     * @return Response|string Returns the current content type or `$this` for fluent calls.
225
     */
226 44
    public function contentType($value = null) {
227 44
        if ($value === null) {
228 44
            return $this->headers('Content-Type');
229
        }
230
231 44
        return $this->headers('Content-Type', $value);
232
    }
233
234
    /**
235
     * Gets or sets the asset that will be rendered in the response.
236
     *
237
     * @param string $asset Set a new value or pass `null` to get the current value.
238
     * @return Response|string Returns the current content asset or `$this` when settings.
239
     */
240 44
    public function contentAsset($asset = null) {
241 44
        if ($asset !== null) {
242 44
            $this->contentAsset = $asset;
243 44
            return $this;
244
        }
245
246 23
        return $this->contentAsset;
247
    }
248
249
    /**
250
     * Set the content type from an accept header.
251
     *
252
     * @param string $accept The value of the accept header.
253
     * @return Response $this Returns `$this` for fluent calls.
254
     */
255 44
    public function contentTypeFromAccept($accept) {
256 44
        if (!empty($this->headers['Content-Type'])) {
257
            return;
258
        }
259
260 44
        $accept = strtolower($accept);
261 44
        if (strpos($accept, ',') === false) {
262 44
            list($contentType) = explode(';', $accept);
263 44
        } elseif (strpos($accept, 'text/html') !== false) {
264
            $contentType = 'text/html';
265
        } elseif (strpos($accept, 'application/rss+xml' !== false)) {
266
            $contentType = 'application/rss+xml';
267
        } elseif (strpos($accept, 'text/plain')) {
268
            $contentType = 'text/plain';
269
        } else {
270
            $contentType = 'text/html';
271
        }
272 44
        $this->contentType($contentType);
273 44
        return $this;
274
    }
275
276
    /**
277
     * Translate an http code to its corresponding status message.
278
     *
279
     * @param int $statusCode The http status code.
280
     * @param bool $header Whether or not the result should be in a form that can be passed to {@link header}.
281
     * @return string Returns the status message corresponding to {@link $code}.
282
     */
283
    public static function statusMessage($statusCode, $header = false) {
284
        $message = val($statusCode, self::$messages, 'Unknown');
285
286
        if ($header) {
287
            return "HTTP/1.1 $statusCode $message";
288
        } else {
289
            return $message;
290
        }
291
    }
292
293
    /**
294
     * Gets or sets a cookie.
295
     *
296
     * @param string $name The name of the cookie.
297
     * @param bool $value The value of the cookie. This value is stored on the clients computer; do not store sensitive information.
298
     * @param int $expires The time the cookie expires. This is a Unix timestamp so is in number of seconds since the epoch.
299
     * @param string $path The path on the server in which the cookie will be available on.
300
     * If set to '/', the cookie will be available within the entire {@link $domain}.
301
     * @param string $domain The domain that the cookie is available to.
302
     * @param bool $secure Indicates that the cookie should only be transmitted over a secure HTTPS connection from the client.
303
     * @param bool $httponly When TRUE the cookie will be made accessible only through the HTTP protocol.
304
     * @return $this|mixed Returns the cookie settings at {@link $name} or `$this` when setting a cookie for fluent calls.
305
     */
306
    public function cookies($name, $value = false, $expires = 0, $path = null, $domain = null, $secure = false, $httponly = false) {
307
        if ($value === false) {
308
            return val($name, $this->cookies);
309
        }
310
311
        $this->cookies[$name] = [$value, $expires, $path, $domain, $secure, $httponly];
312
        return $this;
313
    }
314
315
    /**
316
     * Gets or sets a global cookie.
317
     *
318
     * Global cookies are used when you want to set a cookie, but a {@link Response} has not been created yet.
319
     *
320
     * @param string $name The name of the cookie.
321
     * @param bool $value The value of the cookie. This value is stored on the clients computer; do not store sensitive information.
322
     * @param int $expires The time the cookie expires. This is a Unix timestamp so is in number of seconds since the epoch.
323
     * @param string $path The path on the server in which the cookie will be available on.
324
     * If set to '/', the cookie will be available within the entire {@link $domain}.
325
     * @param string $domain The domain that the cookie is available to.
326
     * @param bool $secure Indicates that the cookie should only be transmitted over a secure HTTPS connection from the client.
327
     * @param bool $httponly When TRUE the cookie will be made accessible only through the HTTP protocol.
328
     * @return mixed|null Returns the cookie settings at {@link $name} or `null` when setting a cookie.
329
     */
330
    public static function globalCookies($name = null, $value = false, $expires = 0, $path = null, $domain = null, $secure = false, $httponly = false) {
331
        if (self::$globalCookies === null) {
332
            self::$globalCookies = [];
333
        }
334
335
        if ($name === null) {
336
            return self::$globalCookies;
337
        }
338
339
        if ($value === false) {
340
            return val($name, self::$globalCookies);
341
        }
342
343
        self::$globalCookies[$name] = [$value, $expires, $path, $domain, $secure, $httponly];
344
        return null;
345
    }
346
347
    /**
348
     * Get or set the meta data for the response.
349
     *
350
     * The meta is an array of data that is unrelated to the response data.
351
     *
352
     * @param array|null $meta Pass a new meta data value or `null` to get the current meta array.
353
     * @param bool $merge Whether or not to merge new data with the current data when setting.
354
     * @return $this|array Returns either the meta or `$this` when setting the meta data.
355
     */
356 44 View Code Duplication
    public function meta($meta = null, $merge = false) {
357 44
        if ($meta !== null) {
358 44
            if ($merge) {
359 44
                $this->meta = array_merge($this->meta, $meta);
360 44
            } else {
361
                $this->meta = $meta;
362
            }
363 44
            return $this;
364
        } else {
365
            return $this->meta;
366
        }
367
    }
368
369
    /**
370
     * Get or set the data for the response.
371
     *
372
     * @param array|null $data Pass a new data value or `null` to get the current data array.
373
     * @param bool $merge Whether or not to merge new data with the current data when setting.
374
     * @return Response|array Returns either the data or `$this` when setting the data.
375
     */
376 44 View Code Duplication
    public function data($data = null, $merge = false) {
377 44
        if ($data !== null) {
378 44
            if ($merge) {
379
                $this->data = array_merge($this->data, $data);
380
            } else {
381 44
                $this->data = $data;
382
            }
383 44
            return $this;
384
        } else {
385
            return $this->data;
386
        }
387
    }
388
389
    /**
390
     * Gets or sets headers.
391
     *
392
     * @param string|array $name The name of the header or an array of headers.
393
     * @param string|null $value A new value for the header or null to get the current header.
394
     * @param bool $replace Whether or not to replace the current header or append.
395
     * @return Response|string Returns the value of the header or `$this` for fluent calls.
396
     */
397 44
    public function headers($name, $value = null, $replace = true) {
398 44
        $headers = static::splitHeaders($name, $value);
399
400 44
        if (is_string($headers)) {
401 44
            return val($headers, $this->headers);
402
        }
403
404 44
        foreach ($headers as $name => $value) {
405 44
            if ($replace || !isset($this->headers[$name])) {
406 44
                $this->headers[$name] = $value;
407 44
            } else {
408
                $this->headers[$name] = array_merge((array)$this->headers, [$value]);
409
            }
410 44
        }
411 44
        return $this;
412
    }
413
414
    /**
415
     * Gets or sets global headers.
416
     *
417
     * The global headers exist to allow code to queue up headers before the response has been constructed.
418
     *
419
     * @param string|array|null $name The name of the header or an array of headers.
420
     * @param string|null $value A new value for the header or null to get the current header.
421
     * @param bool $replace Whether or not to replace the current header or append.
422
     * @return string|array Returns one of the following:
423
     * - string|array: Returns the current value of the header at {@link $name}.
424
     * - array: Returns the entire global headers array when {@link $name} is not passed.
425
     * - null: Returns `null` when setting a global header.
426
     */
427
    public static function globalHeaders($name = null, $value = null, $replace = true) {
428
        if (self::$globalHeaders === null) {
429
            self::$globalHeaders = [
430
                'P3P' => 'CP="CAO PSA OUR"'
431
            ];
432
        }
433
434
        if ($name === null) {
435
            return self::$globalHeaders;
436
        }
437
438
        $headers = static::splitHeaders($name, $value);
439
440
        if (is_string($headers)) {
441
            return val($headers, self::$globalHeaders);
442
        }
443
444
        foreach ($headers as $name => $value) {
445
            if ($replace || !isset(self::$globalHeaders[$name])) {
446
                self::$globalHeaders[$name] = $value;
447
            } else {
448
                self::$globalHeaders[$name] = array_merge((array)self::$globalHeaders, [$value]);
449
            }
450
        }
451
        return null;
452
    }
453
454
    /**
455
     * Split and normalize headers into a form appropriate for {@link $headers} or {@link $globalHeaders}.
456
     *
457
     * @param string|array $name The name of the header or an array of headers.
458
     * @param string|null $value The header value if {@link $name} is a string.
459
     * @return array|string Returns one of the following:
460
     * - array: An array of headers.
461
     * - string: The header name if just a name was passed.
462
     * @throws \InvalidArgumentException Throws an exception if {@link $name} is not a valid string or array.
463
     */
464 44
    protected static function splitHeaders($name, $value = null) {
465 44
        if (is_string($name)) {
466 44
            if (strpos($name, ':') !== false) {
467
                // The name is in the form Header: value.
468
                list($name, $value) = explode(':', $name, 2);
469
                return [static::normalizeHeader(trim($name)) => trim($value)];
470 44
            } elseif ($value !== null) {
471 44
                return [static::normalizeHeader($name) => $value];
472
            } else {
473 44
                return static::normalizeHeader($name);
474
            }
475 23
        } elseif (is_array($name)) {
476 23
            $result = [];
477 23
            foreach ($name as $key => $value) {
478 7
                if (is_numeric($key)) {
479
                    // $value should be a header in the form Header: value.
480
                    list($key, $value) = explode(':', $value, 2);
481
                }
482 7
                $result[static::normalizeHeader(trim($key))] = trim($value);
483 23
            }
484 23
            return $result;
485
        }
486
        throw new \InvalidArgumentException("Argument #1 to splitHeaders() was not valid.", 422);
487
    }
488
489
    /**
490
     * Normalize a header key to the proper casing.
491
     *
492
     * Example:
493
     *
494
     * ```
495
     * echo static::normalizeHeader('CONTENT_TYPE');
496
     *
497
     * // Content-Type
498
     * ```
499
     *
500
     * @param string $name The name of the header.
501
     * @return string Returns the normalized header name.
502
     */
503 44
    public static function normalizeHeader($name) {
504
        static $special = [
505
            'etag' => 'ETag', 'p3p' => 'P3P', 'www-authenticate' => 'WWW-Authenticate',
506
            'x-ua-compatible' => 'X-UA-Compatible'
507 44
        ];
508
509 44
        $name = str_replace(['-', '_'], ' ', strtolower($name));
510 44
        if (isset($special[$name])) {
511
            $name = $special[$name];
512
        } else {
513 44
            $name = str_replace(' ', '-', ucwords($name));
514
        }
515 44
        return $name;
516
    }
517
518
    /**
519
     * Gets/sets the http status code.
520
     *
521
     * @param int $value The new value if setting the http status code.
522
     * @return int The current http status code.
523
     * @throws \InvalidArgumentException The new status is not a valid http status number.
524
     */
525 23
    public function status($value = null) {
526 23
        if ($value !== null) {
527 23
            if (!isset(self::$messages[$value])) {
528
                $this->headers('X-Original-Status', $value);
529
                $value = 500;
530
            }
531 23
            $this->status = (int)$value;
532 23
        }
533 23
        return $this->status;
534
    }
535
536
    /**
537
     * Flush the response to the client.
538
     */
539
    public function flush() {
540
        $this->flushHeaders();
541
542
        echo json_encode($this->data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
543
    }
544
545
    /**
546
     * Flush the headers to the browser.
547
     *
548
     * @param bool $global Whether or not to merge the global headers with this response.
549
     */
550
    public function flushHeaders($global = true) {
551
        if (headers_sent()) {
552
            return;
553
        }
554
555
        if ($global) {
556
            $cookies = array_replace(static::globalCookies(), $this->cookies);
557
            $headers = array_replace(static::globalHeaders(), $this->headers);
558
        } else {
559
            $cookies = $this->cookies;
560
            $headers = $this->headers;
561
        }
562
563
        // Set the cookies first.
564
        foreach ($cookies as $name => $value) {
565
            setcookie(
566
                $name,
567
                $value[0],
568
                $value[1],
569
                $value[2] !== null ? $value[2] : $this->defaultCookiePath,
570
                $value[3] !== null ? $value[3] : $this->defaultCookieDomain,
571
                $value[4],
572
                $value[5]
573
            );
574
        }
575
576
        // Set the response code.
577
        header(static::statusMessage($this->status, true), true, $this->status);
578
579
        $headers = array_filter($headers);
580
581
        // The content type is a special case.
582
        if (isset($headers['Content-Type'])) {
583
            $contentType = (array)$headers['Content-Type'];
584
            header('Content-Type: '.reset($contentType).'; charset=utf8', true);
585
            unset($headers['Content-Type']);
586
        }
587
588
        // Flush the rest of the headers.
589
        foreach ($headers as $name => $value) {
590
            foreach ((array)$value as $hvalue) {
591
                header("$name: $hvalue", false);
592
            }
593
        }
594
    }
595
596
    /**
597
     * Specify data which should be serialized to JSON.
598
     *
599
     * @link http://php.net/manual/en/jsonserializable.jsonserialize.php
600
     * @return mixed data which can be serialized by <b>json_encode</b>,
601
     * which is a value of any type other than a resource.
602
     */
603 23
    public function jsonSerialize() {
604 23
        $asset = (string)$this->contentAsset();
605
606 23
        if ($asset) {
607
            // A specific asset was specified.
608 23
            if (strpos($asset, '.') !== false) {
609 15
                list($group, $key) = explode('.', $asset, 2);
610
                switch ($group) {
611 15
                    case 'meta':
612 15
                        return val($key, $this->meta);
613
                    case 'data':
614
                        return val($key, $this->data);
615
                    default:
616
                        return null;
617
                }
618
            } else {
619
                switch ($asset) {
620 8
                    case 'data':
621 8
                        return $this->data;
622
                    case 'meta':
623
                        return $this->meta;
624
                    default:
625
                        return null;
626
                }
627
            }
628
        }
629
        return $this->data;
630
    }
631
}
632