1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* Response |
5
|
|
|
* |
6
|
|
|
* Handles the HTTP Response for the current execution. |
7
|
|
|
* |
8
|
|
|
* @package core |
9
|
|
|
* @author [email protected] |
10
|
|
|
* @copyright Caffeina srl - 2015 - http://caffeina.it |
11
|
|
|
*/ |
12
|
|
|
|
13
|
|
|
class Response { |
14
|
|
|
use Module, Events; |
15
|
|
|
|
16
|
|
|
const TYPE_JSON = 'application/json', |
17
|
|
|
TYPE_HTML = 'text/html', |
18
|
|
|
TYPE_TEXT = 'text/plain', |
19
|
|
|
TYPE_CSS = 'text/css', |
20
|
|
|
TYPE_XML = 'text/xml', |
21
|
|
|
TYPE_SVG = 'image/svg+xml', |
22
|
|
|
TYPE_JS = 'application/javascript', |
23
|
|
|
TYPE_BIN = 'application/octet-stream'; |
24
|
|
|
|
25
|
|
|
protected static $payload = [], |
26
|
|
|
$status = 200, |
27
|
|
|
$charset = "utf-8", |
28
|
|
|
$headers = ['Content-Type' => ['text/html; charset=utf-8']], |
29
|
|
|
$buffer = null, |
30
|
|
|
$force_dl = false, |
31
|
|
|
$link = null, |
32
|
|
|
$sent = false, |
33
|
|
|
$links = []; |
34
|
|
|
|
35
|
|
|
|
36
|
|
|
public static function charset($charset){ |
37
|
|
|
static::$charset = $charset; |
38
|
|
|
} |
39
|
|
|
|
40
|
|
|
public static function type($mime){ |
41
|
|
|
static::header('Content-Type',$mime . (static::$charset ? '; charset='.static::$charset : '')); |
42
|
|
|
} |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* Force download of Response body |
46
|
|
|
* @param mixed $data Pass a falsy value to disable download, pass a filename for exporting content or array with raw string data |
47
|
|
|
* @return void |
48
|
|
|
*/ |
49
|
|
|
public static function download($data){ |
50
|
|
|
if (is_array($data)) { |
51
|
|
|
if (isset($data['filename'])) static::$force_dl = $data['filename']; |
52
|
|
|
if (isset($data['charset'])) static::charset($data['charset']); |
53
|
|
|
if (isset($data['mime'])) static::type($data['mime']); |
54
|
|
|
if (isset($data['body'])) static::body($data['body']); |
55
|
|
|
} else static::$force_dl = $data; |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* Start capturing output |
60
|
|
|
*/ |
61
|
|
|
public static function start(){ |
62
|
|
|
static::$buffer = ob_start(); |
63
|
|
|
} |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* Enable CORS HTTP headers. |
67
|
|
|
*/ |
68
|
|
|
public static function enableCORS($origin='*'){ |
69
|
|
|
|
70
|
|
|
// Allow from any origin |
71
|
|
|
if ($origin = $origin ?:( isset($_SERVER['HTTP_ORIGIN']) |
72
|
|
|
? $_SERVER['HTTP_ORIGIN'] |
73
|
|
|
: (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '*') |
74
|
|
|
)) { |
75
|
|
|
static::header('Access-Control-Allow-Origin', $origin); |
76
|
|
|
static::header('Access-Control-Allow-Credentials', 'true'); |
77
|
|
|
static::header('Access-Control-Max-Age', 86400); |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
// Access-Control headers are received during OPTIONS requests |
81
|
|
|
if (filter_input(INPUT_SERVER,'REQUEST_METHOD') == 'OPTIONS') { |
82
|
|
|
static::clean(); |
83
|
|
|
|
84
|
|
|
if (filter_input(INPUT_SERVER,'HTTP_ACCESS_CONTROL_REQUEST_METHOD')) { |
85
|
|
|
static::header('Access-Control-Allow-Methods', |
86
|
|
|
'GET, POST, PUT, DELETE, OPTIONS, HEAD, CONNECT, PATCH, TRACE'); |
87
|
|
|
} |
88
|
|
|
if ($req_h = filter_input(INPUT_SERVER,'HTTP_ACCESS_CONTROL_REQUEST_HEADERS')) { |
89
|
|
|
static::header('Access-Control-Allow-Headers',$req_h); |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
self::trigger('cors.preflight'); |
93
|
|
|
static::send(); |
94
|
|
|
exit; |
95
|
|
|
} |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
public static function sent() { |
99
|
|
|
return static::$sent; |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* Finish the output buffer capturing. |
104
|
|
|
* @return string The captured buffer |
105
|
|
|
*/ |
106
|
|
|
public static function end(){ |
107
|
|
|
if (static::$buffer){ |
108
|
|
|
static::$payload[] = ob_get_contents(); |
109
|
|
|
ob_end_clean(); |
110
|
|
|
static::$buffer = null; |
111
|
|
|
return end(static::$payload); |
112
|
|
|
} |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
/** |
116
|
|
|
* Check if an response output buffering is active. |
117
|
|
|
* @return boolean |
118
|
|
|
*/ |
119
|
|
|
public static function isBuffering(){ |
120
|
|
|
return static::$buffer; |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* Clear the response body |
125
|
|
|
*/ |
126
|
|
|
public static function clean(){ |
127
|
|
|
static::$payload = []; |
128
|
|
|
static::$headers = []; |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* Append a JSON object to the buffer. |
133
|
|
|
* @param mixed $payload Data to append to the response buffer |
134
|
|
|
*/ |
135
|
|
|
public static function json($payload){ |
136
|
|
|
static::type(static::TYPE_JSON); |
137
|
|
|
static::$payload[] = json_encode($payload, Options::get('core.response.json_flags',JSON_NUMERIC_CHECK|JSON_BIGINT_AS_STRING)); |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Append a text to the buffer. |
142
|
|
|
* @param mixed $payload Text to append to the response buffer |
143
|
|
|
*/ |
144
|
|
|
public static function text(...$args){ |
145
|
|
|
static::type(static::TYPE_TEXT); |
146
|
|
|
static::$payload[] = implode('',$args); |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
/** |
150
|
|
|
* Append an XML string to the buffer. |
151
|
|
|
* @param mixed $payload Data to append to the response buffer |
|
|
|
|
152
|
|
|
*/ |
153
|
|
|
public static function xml(...$args){ |
154
|
|
|
static::type(static::TYPE_XML); |
155
|
|
|
static::$payload[] = implode('', $args); |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* Append a SVG string to the buffer. |
160
|
|
|
* @param mixed $payload Data to append to the response buffer |
|
|
|
|
161
|
|
|
*/ |
162
|
|
|
public static function svg(...$args){ |
163
|
|
|
static::type(static::TYPE_SVG); |
164
|
|
|
static::$payload[] = implode('', $args); |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
/** |
168
|
|
|
* Append an HTML string to the buffer. |
169
|
|
|
* @param mixed $payload Data to append to the response buffer |
|
|
|
|
170
|
|
|
*/ |
171
|
|
|
public static function html(...$args){ |
172
|
|
|
static::type(static::TYPE_HTML); |
173
|
|
|
static::$payload[] = implode('', $args); |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* Append data to the buffer. |
178
|
|
|
* Rules : |
179
|
|
|
* - Callables will be called and their results added (recursive) |
180
|
|
|
* - Views will be rendered |
181
|
|
|
* - Objects, arrays and bools will be JSON encoded |
182
|
|
|
* - Strings and numbers will be appendend to the response |
183
|
|
|
* |
184
|
|
|
* @param mixed $payload Data to append to the response buffer |
|
|
|
|
185
|
|
|
*/ |
186
|
|
|
public static function add(){ |
187
|
|
|
foreach(func_get_args() as $data){ |
188
|
|
|
switch (true) { |
189
|
|
|
case is_callable($data) : |
190
|
|
|
return static::add($data()); |
191
|
|
|
case is_a($data, 'View') : |
192
|
|
|
return static::$payload[] = "$data"; |
193
|
|
|
case is_object($data) || is_array($data) || is_bool($data): |
194
|
|
|
return static::json($data); |
195
|
|
|
default: |
196
|
|
|
return static::$payload[] = $data; |
197
|
|
|
} |
198
|
|
|
} |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
public static function status($code,$message=''){ |
202
|
|
|
static::header('Status',$message?:$code,$code); |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
public static function header($name,$value,$code=null){ |
206
|
|
|
if (empty(static::$headers[$name])){ |
207
|
|
|
static::$headers[$name] = [[$value,$code]]; |
208
|
|
|
} else { |
209
|
|
|
static::$headers[$name][] = [$value,$code]; |
210
|
|
|
} |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
public static function error($code=500,$message='Application Error'){ |
214
|
|
|
static::trigger('error',$code,$message); |
215
|
|
|
Event::trigger('core.response.error',$code,$message); |
216
|
|
|
static::status($code,$message); |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
public static function body($setBody=null){ |
220
|
|
|
if ($setBody) static::$payload = [$setBody]; |
221
|
|
|
return Filter::with('core.response.body', |
222
|
|
|
is_array(static::$payload) ? implode('',static::$payload) : static::$payload |
223
|
|
|
); |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
public static function headers($setHeaders=null){ |
227
|
|
|
if ($setHeaders) static::$headers = $setHeaders; |
228
|
|
|
return static::$headers; |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
/** |
232
|
|
|
* Save response as an object, for serialization or cache storage |
233
|
|
|
* |
234
|
|
|
* @method save |
235
|
|
|
* |
236
|
|
|
* @return array Headers and body of the response |
237
|
|
|
*/ |
238
|
|
|
public static function save(){ |
239
|
|
|
return [ |
240
|
|
|
'head' => static::$headers, |
241
|
|
|
'body' => static::body(), |
242
|
|
|
]; |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* Load response from a saved state |
247
|
|
|
* |
248
|
|
|
* @method load |
249
|
|
|
* |
250
|
|
|
* @param array $data head/body saved state |
251
|
|
|
*/ |
252
|
|
|
public static function load($data){ |
253
|
|
|
$data = (object)$data; |
254
|
|
|
if (isset($data->head)) static::headers($data->head); |
255
|
|
|
if (isset($data->body)) static::body($data->body); |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
public static function send($force = false){ |
259
|
|
|
if (!static::$sent || $force) { |
260
|
|
|
static::$sent = true; |
261
|
|
|
static::trigger('send'); |
262
|
|
|
Event::trigger('core.response.send'); |
263
|
|
|
if (false === headers_sent()) foreach (static::$headers as $name => $family) |
264
|
|
|
foreach ($family as $value_code) { |
265
|
|
|
|
266
|
|
|
if (is_array($value_code)) { |
267
|
|
|
list($value, $code) = (count($value_code) > 1) ? $value_code : [current($value_code), 200]; |
268
|
|
|
} else { |
269
|
|
|
$value = $value_code; |
270
|
|
|
$code = null; |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
switch($value){ |
274
|
|
|
case "Status": |
275
|
|
|
if (function_exists('http_response_code')){ |
276
|
|
|
http_response_code($code); |
277
|
|
|
} else { |
278
|
|
|
header("Status: $code", true, $code); |
279
|
|
|
} |
280
|
|
|
break; |
281
|
|
|
case "Link": |
282
|
|
|
header("Link: $value", false); |
283
|
|
|
break; |
284
|
|
|
default: |
285
|
|
|
if ($code) { |
286
|
|
|
header("$name: $value", true, $code); |
287
|
|
|
} else { |
288
|
|
|
header("$name: $value", true); |
289
|
|
|
} |
290
|
|
|
break; |
291
|
|
|
} |
292
|
|
|
} |
293
|
|
|
if (static::$force_dl) header('Content-Disposition: attachment; filename="'.static::$force_dl.'"'); |
294
|
|
|
echo static::body(); |
295
|
|
|
static::trigger('sent'); |
296
|
|
|
} |
297
|
|
|
} |
298
|
|
|
|
299
|
|
|
|
300
|
|
|
/** |
301
|
|
|
* Push resources to client (HTTP/2 spec) |
302
|
|
|
* @param string/array $links The link(s) to the resources to push. |
|
|
|
|
303
|
|
|
* @return Response The Route object |
304
|
|
|
*/ |
305
|
|
|
public static function push($links, $type='text'){ |
306
|
|
|
if (is_array($links)){ |
307
|
|
|
foreach($links as $_type => $link) { |
308
|
|
|
// Extract URL basename extension (query-safe version) |
309
|
|
|
if (is_numeric($_type)) switch(strtolower(substr(strrchr(strtok(basename($link),'?'),'.'),1))) { |
310
|
|
|
case 'js': $_type = 'script'; break; |
311
|
|
|
case 'css': $_type = 'style'; break; |
312
|
|
|
case 'png': case 'svg': case 'gif': case 'jpg': $_type = 'image'; break; |
313
|
|
|
case 'woff': case 'woff2': case 'ttf': case 'eof': $_type = 'font'; break; |
314
|
|
|
default: $_type = 'text'; break; |
315
|
|
|
} |
316
|
|
|
foreach ((array)$link as $link_val) { |
317
|
|
|
static::header("Link","<$link_val>; rel=preload; as=$_type"); |
318
|
|
|
} |
319
|
|
|
} |
320
|
|
|
} else { |
321
|
|
|
static::header("Link","<".((string)$links).">; rel=preload; as=$type"); |
322
|
|
|
} |
323
|
|
|
} |
324
|
|
|
|
325
|
|
|
} |
326
|
|
|
|
This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.
Consider the following example. The parameter
$italy
is not defined by the methodfinale(...)
.The most likely cause is that the parameter was removed, but the annotation was not.