|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* Bluz Framework Component |
|
4
|
|
|
* |
|
5
|
|
|
* @copyright Bluz PHP Team |
|
6
|
|
|
* @link https://github.com/bluzphp/framework |
|
7
|
|
|
*/ |
|
8
|
|
|
|
|
9
|
|
|
/** |
|
10
|
|
|
* @namespace |
|
11
|
|
|
*/ |
|
12
|
|
|
namespace Bluz\Response; |
|
13
|
|
|
|
|
14
|
|
|
use Bluz\Application\Exception\NotAcceptableException; |
|
15
|
|
|
use Bluz\Cli\CliResponse; |
|
16
|
|
|
use Bluz\Common\Options; |
|
17
|
|
|
use Bluz\Controller\Controller; |
|
18
|
|
|
use Bluz\Layout\Layout; |
|
19
|
|
|
use Bluz\Proxy\Messages; |
|
20
|
|
|
use Bluz\Proxy\Request; |
|
21
|
|
|
use Zend\Diactoros\Response\EmptyResponse; |
|
22
|
|
|
use Zend\Diactoros\Response\HtmlResponse; |
|
23
|
|
|
use Zend\Diactoros\Response\JsonResponse; |
|
24
|
|
|
use Zend\Diactoros\Response\RedirectResponse; |
|
25
|
|
|
use Zend\Diactoros\Response\SapiEmitter; |
|
26
|
|
|
|
|
27
|
|
|
/** |
|
28
|
|
|
* Response Container |
|
29
|
|
|
* |
|
30
|
|
|
* @package Bluz\Response |
|
31
|
|
|
* @author Anton Shevchuk |
|
32
|
|
|
* @link https://github.com/bluzphp/framework/wiki/Response |
|
33
|
|
|
*/ |
|
34
|
|
|
class Response |
|
35
|
|
|
{ |
|
36
|
|
|
use Options; |
|
37
|
|
|
|
|
38
|
|
|
/** |
|
39
|
|
|
* @var string HTTP protocol version |
|
40
|
|
|
*/ |
|
41
|
|
|
protected $protocol = '1.1'; |
|
42
|
|
|
|
|
43
|
|
|
/** |
|
44
|
|
|
* @var integer response code equal to HTTP status codes |
|
45
|
|
|
*/ |
|
46
|
|
|
protected $code = 200; |
|
47
|
|
|
|
|
48
|
|
|
/** |
|
49
|
|
|
* @var string|null HTTP Phrase |
|
50
|
|
|
*/ |
|
51
|
|
|
protected $phrase; |
|
52
|
|
|
|
|
53
|
|
|
/** |
|
54
|
|
|
* @var array list of headers |
|
55
|
|
|
*/ |
|
56
|
|
|
protected $headers = array(); |
|
57
|
|
|
|
|
58
|
|
|
/** |
|
59
|
|
|
* @var array list of cookies |
|
60
|
|
|
*/ |
|
61
|
|
|
protected $cookies = array(); |
|
62
|
|
|
|
|
63
|
|
|
/** |
|
64
|
|
|
* @var mixed result can be Controller|Layout |
|
65
|
|
|
*/ |
|
66
|
|
|
protected $body; |
|
67
|
|
|
|
|
68
|
|
|
/** |
|
69
|
|
|
* @var string CLI|HTML|JSON |
|
70
|
|
|
*/ |
|
71
|
|
|
protected $type = 'HTML'; |
|
72
|
|
|
|
|
73
|
|
|
/** |
|
74
|
|
|
* send |
|
75
|
|
|
* |
|
76
|
|
|
* @throws NotAcceptableException |
|
77
|
|
|
*/ |
|
78
|
|
|
public function send() |
|
79
|
|
|
{ |
|
80
|
|
|
$body = $this->getBody(); |
|
81
|
|
|
|
|
82
|
|
|
$this->sendCookies(); |
|
83
|
|
|
|
|
84
|
|
|
switch (true) { |
|
85
|
|
|
case 'CLI' == $this->type: |
|
86
|
|
|
// CLI response |
|
87
|
|
|
$response = new CliResponse( |
|
88
|
|
|
$body->render('CLI'), |
|
|
|
|
|
|
89
|
|
|
$this->getStatusCode() |
|
90
|
|
|
); |
|
91
|
|
|
break; |
|
92
|
|
|
case is_null($body): |
|
93
|
|
|
case 204 == $this->getStatusCode(): |
|
94
|
|
|
$response = new EmptyResponse($this->getStatusCode(), $this->getHeaders()); |
|
95
|
|
|
break; |
|
96
|
|
|
case 301 == $this->getStatusCode(): |
|
97
|
|
|
case 302 == $this->getStatusCode(): |
|
98
|
|
|
$response = new RedirectResponse( |
|
99
|
|
|
$this->getHeader('Location'), |
|
100
|
|
|
$this->getStatusCode(), |
|
101
|
|
|
$this->getHeaders() |
|
102
|
|
|
); |
|
103
|
|
|
break; |
|
104
|
|
|
case 'JSON' == $this->type: |
|
105
|
|
|
// JSON response |
|
106
|
|
|
// setup messages |
|
107
|
|
|
if (Messages::count()) { |
|
108
|
|
|
$this->setHeader('Bluz-Notify', json_encode(Messages::popAll())); |
|
109
|
|
|
} |
|
110
|
|
|
|
|
111
|
|
|
// encode body data to JSON |
|
112
|
|
|
$response = new JsonResponse( |
|
113
|
|
|
$body, |
|
114
|
|
|
$this->getStatusCode(), |
|
115
|
|
|
$this->getHeaders() |
|
116
|
|
|
); |
|
117
|
|
|
break; |
|
118
|
|
|
case 'HTML' == $this->type: |
|
119
|
|
|
default: |
|
120
|
|
|
// HTML response |
|
121
|
|
|
$response = new HtmlResponse( |
|
122
|
|
|
(string) $body, |
|
123
|
|
|
$this->getStatusCode(), |
|
124
|
|
|
$this->getHeaders() |
|
125
|
|
|
); |
|
126
|
|
|
break; |
|
127
|
|
|
} |
|
128
|
|
|
|
|
129
|
|
|
$emitter = new SapiEmitter(); |
|
130
|
|
|
$emitter->emit($response); |
|
131
|
|
|
} |
|
132
|
|
|
|
|
133
|
|
|
/** |
|
134
|
|
|
* Set Response Type, one of JSON, HTML or CLI |
|
135
|
|
|
* |
|
136
|
|
|
* @param $type |
|
137
|
|
|
*/ |
|
138
|
|
|
public function switchType($type) |
|
139
|
|
|
{ |
|
140
|
|
|
// switch statement for $type |
|
|
|
|
|
|
141
|
|
|
switch ($type) { |
|
142
|
|
|
case 'JSON': |
|
143
|
|
|
$this->setHeader('Content-Type', 'application/json'); |
|
144
|
|
|
break; |
|
145
|
|
|
case 'CLI': |
|
146
|
|
|
case 'HTML': |
|
147
|
|
|
default: |
|
148
|
|
|
break; |
|
149
|
|
|
} |
|
150
|
|
|
|
|
151
|
|
|
$this->type = $type; |
|
152
|
|
|
} |
|
153
|
|
|
|
|
154
|
|
|
/** |
|
155
|
|
|
* Gets the HTTP protocol version as a string |
|
156
|
|
|
* |
|
157
|
|
|
* The string MUST contain only the HTTP version number (e.g., "1.1", "1.0"). |
|
158
|
|
|
* |
|
159
|
|
|
* @return string HTTP protocol version. |
|
160
|
|
|
*/ |
|
161
|
1 |
|
public function getProtocolVersion() |
|
162
|
|
|
{ |
|
163
|
1 |
|
return $this->protocol; |
|
164
|
|
|
} |
|
165
|
|
|
|
|
166
|
|
|
/** |
|
167
|
|
|
* Gets the response Status-Code |
|
168
|
|
|
* |
|
169
|
|
|
* The Status-Code is a 3-digit integer result code of the server's attempt |
|
170
|
|
|
* to understand and satisfy the request. |
|
171
|
|
|
* |
|
172
|
|
|
* @return integer status code. |
|
173
|
|
|
*/ |
|
174
|
|
|
public function getStatusCode() |
|
175
|
|
|
{ |
|
176
|
|
|
return $this->code; |
|
177
|
|
|
} |
|
178
|
|
|
|
|
179
|
|
|
/** |
|
180
|
|
|
* Sets the status code of this response |
|
181
|
|
|
* |
|
182
|
|
|
* @param integer $code the 3-digit integer result code to set. |
|
183
|
|
|
* @return void |
|
184
|
|
|
*/ |
|
185
|
1 |
|
public function setStatusCode($code) |
|
186
|
|
|
{ |
|
187
|
1 |
|
$this->code = (int) $code; |
|
188
|
1 |
|
} |
|
189
|
|
|
|
|
190
|
|
|
/** |
|
191
|
|
|
* Gets the response Reason-Phrase, a short textual description of the Status-Code |
|
192
|
|
|
* |
|
193
|
|
|
* Because a Reason-Phrase is not a required element in response |
|
194
|
|
|
* Status-Line, the Reason-Phrase value MAY be null. Implementations MAY |
|
195
|
|
|
* choose to return the default RFC 2616 recommended reason phrase for the |
|
196
|
|
|
* response's Status-Code. |
|
197
|
|
|
* |
|
198
|
|
|
* @return string|null reason phrase, or null if unknown. |
|
199
|
|
|
*/ |
|
200
|
2 |
|
public function getReasonPhrase() |
|
201
|
|
|
{ |
|
202
|
2 |
|
return $this->phrase; |
|
203
|
|
|
} |
|
204
|
|
|
|
|
205
|
|
|
/** |
|
206
|
|
|
* Sets the Reason-Phrase of the response |
|
207
|
|
|
* |
|
208
|
|
|
* If no Reason-Phrase is specified, implementations MAY choose to default |
|
209
|
|
|
* to the RFC 2616 recommended reason phrase for the response's Status-Code. |
|
210
|
|
|
* |
|
211
|
|
|
* @param string $phrase the Reason-Phrase to set. |
|
212
|
|
|
*/ |
|
213
|
1 |
|
public function setReasonPhrase($phrase) |
|
214
|
|
|
{ |
|
215
|
1 |
|
$this->phrase = $phrase; |
|
216
|
1 |
|
} |
|
217
|
|
|
|
|
218
|
|
|
/** |
|
219
|
|
|
* Retrieve a header by the given case-insensitive name as a string |
|
220
|
|
|
* |
|
221
|
|
|
* This method returns all of the header values of the given |
|
222
|
|
|
* case-insensitive header name as a string concatenated together using |
|
223
|
|
|
* a comma. |
|
224
|
|
|
* |
|
225
|
|
|
* @param string $header case-insensitive header name. |
|
226
|
|
|
* @return string |
|
227
|
|
|
*/ |
|
228
|
2 |
|
public function getHeader($header) |
|
229
|
|
|
{ |
|
230
|
2 |
|
if ($this->hasHeader($header)) { |
|
231
|
2 |
|
return join(', ', $this->headers[$header]); |
|
232
|
|
|
} else { |
|
233
|
1 |
|
return ''; |
|
234
|
|
|
} |
|
235
|
|
|
} |
|
236
|
|
|
|
|
237
|
|
|
/** |
|
238
|
|
|
* Retrieves a header by the given case-insensitive name as an array of strings |
|
239
|
|
|
* |
|
240
|
|
|
* @param string $header Case-insensitive header name. |
|
241
|
|
|
* @return string[] |
|
242
|
|
|
*/ |
|
243
|
1 |
|
public function getHeaderAsArray($header) |
|
244
|
|
|
{ |
|
245
|
1 |
|
if ($this->hasHeader($header)) { |
|
246
|
1 |
|
return $this->headers[$header]; |
|
247
|
|
|
} else { |
|
248
|
1 |
|
return array(); |
|
249
|
|
|
} |
|
250
|
|
|
} |
|
251
|
|
|
|
|
252
|
|
|
/** |
|
253
|
|
|
* Checks if a header exists by the given case-insensitive name |
|
254
|
|
|
* |
|
255
|
|
|
* @param string $header case-insensitive header name. |
|
256
|
|
|
* @return bool returns true if any header names match the given header |
|
257
|
|
|
* name using a case-insensitive string comparison. Returns false if |
|
258
|
|
|
* no matching header name is found in the message. |
|
259
|
|
|
*/ |
|
260
|
1 |
|
public function hasHeader($header) |
|
261
|
|
|
{ |
|
262
|
1 |
|
return isset($this->headers[$header]); |
|
263
|
|
|
} |
|
264
|
|
|
|
|
265
|
|
|
/** |
|
266
|
|
|
* Sets a header, replacing any existing values of any headers with the |
|
267
|
|
|
* same case-insensitive name |
|
268
|
|
|
* |
|
269
|
|
|
* The header name is case-insensitive. The header values MUST be a string |
|
270
|
|
|
* or an array of strings. |
|
271
|
|
|
* |
|
272
|
|
|
* @param string $header header name |
|
273
|
|
|
* @param string|string[] $value header value(s) |
|
274
|
|
|
* @return void |
|
275
|
|
|
*/ |
|
276
|
1 |
|
public function setHeader($header, $value) |
|
277
|
|
|
{ |
|
278
|
1 |
|
$this->headers[$header] = (array) $value; |
|
279
|
1 |
|
} |
|
280
|
|
|
|
|
281
|
|
|
/** |
|
282
|
|
|
* Appends a header value for the specified header |
|
283
|
|
|
* |
|
284
|
|
|
* Existing values for the specified header will be maintained. The new |
|
285
|
|
|
* value will be appended to the existing list. |
|
286
|
|
|
* |
|
287
|
|
|
* @param string $header header name to add |
|
288
|
|
|
* @param string $value value of the header |
|
289
|
|
|
* @return void |
|
290
|
|
|
*/ |
|
291
|
1 |
|
public function addHeader($header, $value) |
|
292
|
|
|
{ |
|
293
|
1 |
|
if ($this->hasHeader($header)) { |
|
294
|
1 |
|
$this->headers[$header][] = $value; |
|
295
|
|
|
} else { |
|
296
|
1 |
|
$this->setHeader($header, $value); |
|
297
|
|
|
} |
|
298
|
1 |
|
} |
|
299
|
|
|
|
|
300
|
|
|
/** |
|
301
|
|
|
* Remove a specific header by case-insensitive name. |
|
302
|
|
|
* |
|
303
|
|
|
* @param string $header HTTP header to remove |
|
304
|
|
|
* @return void |
|
305
|
|
|
*/ |
|
306
|
1 |
|
public function removeHeader($header) |
|
307
|
|
|
{ |
|
308
|
1 |
|
unset($this->headers[$header]); |
|
309
|
1 |
|
} |
|
310
|
|
|
|
|
311
|
|
|
/** |
|
312
|
|
|
* Gets all message headers |
|
313
|
|
|
* |
|
314
|
|
|
* The keys represent the header name as it will be sent over the wire, and |
|
315
|
|
|
* each value is an array of strings associated with the header. |
|
316
|
|
|
* |
|
317
|
|
|
* // Represent the headers as a string |
|
318
|
|
|
* foreach ($message->getHeaders() as $name => $values) { |
|
319
|
|
|
* echo $name . ": " . implode(", ", $values); |
|
320
|
|
|
* } |
|
321
|
|
|
* |
|
322
|
|
|
* @return array returns an associative array of the message's headers. |
|
323
|
|
|
*/ |
|
324
|
1 |
|
public function getHeaders() |
|
325
|
|
|
{ |
|
326
|
1 |
|
return $this->headers; |
|
327
|
|
|
} |
|
328
|
|
|
|
|
329
|
|
|
/** |
|
330
|
|
|
* Sets headers, replacing any headers that have already been set on the message |
|
331
|
|
|
* |
|
332
|
|
|
* The array keys MUST be a string. The array values must be either a |
|
333
|
|
|
* string or an array of strings. |
|
334
|
|
|
* |
|
335
|
|
|
* @param array $headers Headers to set. |
|
336
|
|
|
* @return void |
|
337
|
|
|
*/ |
|
338
|
1 |
|
public function setHeaders(array $headers) |
|
339
|
|
|
{ |
|
340
|
1 |
|
$this->headers = $headers; |
|
341
|
1 |
|
} |
|
342
|
|
|
|
|
343
|
|
|
/** |
|
344
|
|
|
* Merges in an associative array of headers. |
|
345
|
|
|
* |
|
346
|
|
|
* Each array key MUST be a string representing the case-insensitive name |
|
347
|
|
|
* of a header. Each value MUST be either a string or an array of strings. |
|
348
|
|
|
* For each value, the value is appended to any existing header of the same |
|
349
|
|
|
* name, or, if a header does not already exist by the given name, then the |
|
350
|
|
|
* header is added. |
|
351
|
|
|
* |
|
352
|
|
|
* @param array $headers Associative array of headers to add to the message |
|
353
|
|
|
* @return void |
|
354
|
|
|
*/ |
|
355
|
1 |
|
public function addHeaders(array $headers) |
|
356
|
|
|
{ |
|
357
|
1 |
|
$this->headers = array_merge_recursive($this->headers, $headers); |
|
358
|
1 |
|
} |
|
359
|
|
|
|
|
360
|
|
|
/** |
|
361
|
|
|
* Remove all headers |
|
362
|
|
|
* |
|
363
|
|
|
* @return void |
|
364
|
|
|
*/ |
|
365
|
2 |
|
public function removeHeaders() |
|
366
|
|
|
{ |
|
367
|
2 |
|
$this->headers = array(); |
|
368
|
2 |
|
} |
|
369
|
|
|
|
|
370
|
|
|
/** |
|
371
|
|
|
* Set response body |
|
372
|
|
|
* |
|
373
|
|
|
* @param mixed $body |
|
374
|
|
|
* @return void |
|
375
|
|
|
*/ |
|
376
|
2 |
|
public function setBody($body) |
|
377
|
|
|
{ |
|
378
|
2 |
|
$this->body = $body; |
|
379
|
2 |
|
} |
|
380
|
|
|
|
|
381
|
|
|
/** |
|
382
|
|
|
* Get response body |
|
383
|
|
|
* |
|
384
|
|
|
* @return Controller|Layout |
|
385
|
|
|
*/ |
|
386
|
|
|
public function getBody() |
|
387
|
|
|
{ |
|
388
|
|
|
return $this->body; |
|
389
|
|
|
} |
|
390
|
|
|
|
|
391
|
|
|
/** |
|
392
|
|
|
* Clear response body |
|
393
|
|
|
* |
|
394
|
|
|
* @return void |
|
395
|
|
|
*/ |
|
396
|
1 |
|
public function clearBody() |
|
397
|
|
|
{ |
|
398
|
1 |
|
$this->body = null; |
|
399
|
1 |
|
} |
|
400
|
|
|
|
|
401
|
|
|
/** |
|
402
|
|
|
* Set Cookie |
|
403
|
|
|
* |
|
404
|
|
|
* @param string $name |
|
405
|
|
|
* @param string $value |
|
406
|
|
|
* @param int|string|\DateTime $expire |
|
407
|
|
|
* @param string $path |
|
408
|
|
|
* @param string $domain |
|
409
|
|
|
* @param bool $secure |
|
410
|
|
|
* @param bool $httpOnly |
|
411
|
|
|
* @return void |
|
412
|
|
|
*/ |
|
413
|
5 |
|
public function setCookie( |
|
414
|
|
|
$name, |
|
415
|
|
|
$value = '', |
|
416
|
|
|
$expire = 0, |
|
417
|
|
|
$path = '/', |
|
418
|
|
|
$domain = '', |
|
419
|
|
|
$secure = false, |
|
420
|
|
|
$httpOnly = false |
|
421
|
|
|
) { |
|
422
|
|
|
// from PHP source code |
|
423
|
5 |
|
if (preg_match("/[=,; \t\r\n\013\014]/", $name)) { |
|
424
|
1 |
|
throw new \InvalidArgumentException('The cookie name contains invalid characters.'); |
|
425
|
|
|
} |
|
426
|
|
|
|
|
427
|
4 |
|
if (empty($name)) { |
|
428
|
1 |
|
throw new \InvalidArgumentException('The cookie name cannot be empty.'); |
|
429
|
|
|
} |
|
430
|
|
|
|
|
431
|
|
|
// convert expiration time to a Unix timestamp |
|
432
|
3 |
|
if ($expire instanceof \DateTime) { |
|
433
|
1 |
|
$expire = $expire->format('U'); |
|
434
|
2 |
|
} elseif (!is_numeric($expire)) { |
|
435
|
1 |
|
$expire = strtotime($expire); |
|
436
|
1 |
|
if (false === $expire || -1 === $expire) { |
|
437
|
1 |
|
throw new \InvalidArgumentException('The cookie expiration time is not valid.'); |
|
438
|
|
|
} |
|
439
|
|
|
} |
|
440
|
|
|
|
|
441
|
2 |
|
$this->cookies[$name] = [ |
|
442
|
2 |
|
'name' => $name, |
|
443
|
2 |
|
'value' => $value, |
|
444
|
2 |
|
'expire' => $expire, |
|
445
|
2 |
|
'path' => $path, |
|
446
|
2 |
|
'domain' => $domain, |
|
447
|
2 |
|
'secure' => (bool) $secure, |
|
448
|
2 |
|
'httpOnly' => (bool) $httpOnly |
|
449
|
|
|
]; |
|
450
|
2 |
|
} |
|
451
|
|
|
|
|
452
|
|
|
/** |
|
453
|
|
|
* Get Cookie by name |
|
454
|
|
|
* |
|
455
|
|
|
* @param string $name |
|
456
|
|
|
* @return array|null |
|
457
|
|
|
*/ |
|
458
|
2 |
|
public function getCookie($name) |
|
459
|
|
|
{ |
|
460
|
2 |
|
return isset($this->cookies[$name])?$this->cookies[$name]:null; |
|
461
|
|
|
} |
|
462
|
|
|
|
|
463
|
|
|
/** |
|
464
|
|
|
* Process Cookies to Header |
|
465
|
|
|
* |
|
466
|
|
|
* Set-Cookie: <name>=<value>[; <name>=<value>]... |
|
467
|
|
|
* [; expires=<date>][; domain=<domain_name>] |
|
468
|
|
|
* [; path=<some_path>][; secure][; httponly] |
|
469
|
|
|
* |
|
470
|
|
|
* @return void |
|
471
|
|
|
*/ |
|
472
|
|
|
protected function sendCookies() |
|
473
|
|
|
{ |
|
474
|
|
|
foreach ($this->cookies as $cookie) { |
|
475
|
|
|
setcookie(...array_values($cookie)); |
|
476
|
|
|
} |
|
477
|
|
|
} |
|
478
|
|
|
} |
|
479
|
|
|
|
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.
In this case you can add the
@ignorePhpDoc annotation to the duplicate definition and it will be ignored.