/
Response.php
324 lines (285 loc) · 6.93 KB
/
Response.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
<?php
namespace Kirby\Http;
use Closure;
use Exception;
use Kirby\Exception\LogicException;
use Kirby\Filesystem\F;
use Throwable;
/**
* Representation of an Http response,
* to simplify sending correct headers
* and Http status codes.
*
* @package Kirby Http
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*/
class Response
{
/**
* Store for all registered headers,
* which will be sent with the response
*/
protected array $headers = [];
/**
* The response body
*/
protected string $body;
/**
* The HTTP response code
*/
protected int $code;
/**
* The content type for the response
*/
protected string $type;
/**
* The content type charset
*/
protected string $charset = 'UTF-8';
/**
* Creates a new response object
*/
public function __construct(
string|array $body = '',
string|null $type = null,
int|null $code = null,
array|null $headers = null,
string|null $charset = null
) {
// array construction
if (is_array($body) === true) {
$params = $body;
$body = $params['body'] ?? '';
$type = $params['type'] ?? $type;
$code = $params['code'] ?? $code;
$headers = $params['headers'] ?? $headers;
$charset = $params['charset'] ?? $charset;
}
// regular construction
$this->body = $body;
$this->type = $type ?? 'text/html';
$this->code = $code ?? 200;
$this->headers = $headers ?? [];
$this->charset = $charset ?? 'UTF-8';
// automatic mime type detection
if (strpos($this->type, '/') === false) {
$this->type = F::extensionToMime($this->type) ?? 'text/html';
}
}
/**
* Improved `var_dump` output
*/
public function __debugInfo(): array
{
return $this->toArray();
}
/**
* Makes it possible to convert the
* entire response object to a string
* to send the headers and print the body
*/
public function __toString(): string
{
try {
return $this->send();
} catch (Throwable) {
return '';
}
}
/**
* Getter for the body
*/
public function body(): string
{
return $this->body;
}
/**
* Getter for the content type charset
*/
public function charset(): string
{
return $this->charset;
}
/**
* Getter for the HTTP status code
*/
public function code(): int
{
return $this->code;
}
/**
* Creates a response that triggers
* a file download for the given file
*
* @param array $props Custom overrides for response props (e.g. headers)
*/
public static function download(
string $file,
string|null $filename = null,
array $props = []
): static {
if (file_exists($file) === false) {
throw new Exception('The file could not be found');
}
$filename ??= basename($file);
$modified = filemtime($file);
$body = file_get_contents($file);
$size = strlen($body);
$props = array_replace_recursive([
'body' => $body,
'type' => F::mime($file),
'headers' => [
'Pragma' => 'public',
'Cache-Control' => 'no-cache, no-store, must-revalidate',
'Last-Modified' => gmdate('D, d M Y H:i:s', $modified) . ' GMT',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
'Content-Transfer-Encoding' => 'binary',
'Content-Length' => $size,
'Connection' => 'close'
]
], $props);
return new static($props);
}
/**
* Creates a response for a file and
* sends the file content to the browser
*
* @param array $props Custom overrides for response props (e.g. headers)
*/
public static function file(string $file, array $props = []): static
{
$props = array_merge([
'body' => F::read($file),
'type' => F::extensionToMime(F::extension($file))
], $props);
// if we couldn't serve a correct MIME type, force
// the browser to display the file as plain text to
// harden against attacks from malicious file uploads
if ($props['type'] === null) {
if (isset($props['headers']) !== true) {
$props['headers'] = [];
}
$props['type'] = 'text/plain';
$props['headers']['X-Content-Type-Options'] = 'nosniff';
}
return new static($props);
}
/**
* Redirects to the given Urls
* Urls can be relative or absolute.
* @since 3.7.0
*
* @codeCoverageIgnore
* @todo Change return type to `never` once support for PHP 8.0 is dropped
*/
public static function go(string $url = '/', int $code = 302): void
{
die(static::redirect($url, $code));
}
/**
* Ensures that the callback does not produce the first body output
* (used to show when loading a file creates side effects)
*/
public static function guardAgainstOutput(Closure $callback, ...$args): mixed
{
$before = headers_sent();
$result = $callback(...$args);
$after = headers_sent($file, $line);
if ($before === false && $after === true) {
throw new LogicException("Disallowed output from file $file:$line, possible accidental whitespace?");
}
return $result;
}
/**
* Getter for single headers
*
* @param string $key Name of the header
*/
public function header(string $key): string|null
{
return $this->headers[$key] ?? null;
}
/**
* Getter for all headers
*/
public function headers(): array
{
return $this->headers;
}
/**
* Creates a json response with appropriate
* header and automatic conversion of arrays.
*/
public static function json(
string|array $body = '',
int|null $code = null,
bool|null $pretty = null,
array $headers = []
): static {
if (is_array($body) === true) {
$body = json_encode($body, $pretty === true ? JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES : 0);
}
return new static([
'body' => $body,
'code' => $code,
'type' => 'application/json',
'headers' => $headers
]);
}
/**
* Creates a redirect response,
* which will send the visitor to the
* given location.
*/
public static function redirect(string $location = '/', int $code = 302): static
{
return new static([
'code' => $code,
'headers' => [
'Location' => Url::unIdn($location)
]
]);
}
/**
* Sends all registered headers and
* returns the response body
*/
public function send(): string
{
// send the status response code
http_response_code($this->code());
// send all custom headers
foreach ($this->headers() as $key => $value) {
header($key . ': ' . $value);
}
// send the content type header
header('Content-Type:' . $this->type() . '; charset=' . $this->charset());
// print the response body
return $this->body();
}
/**
* Converts all relevant response attributes
* to an associative array for debugging,
* testing or whatever.
*/
public function toArray(): array
{
return [
'type' => $this->type(),
'charset' => $this->charset(),
'code' => $this->code(),
'headers' => $this->headers(),
'body' => $this->body()
];
}
/**
* Getter for the content type
*/
public function type(): string
{
return $this->type;
}
}