1 <?php
2
3 4 5 6 7
8 class rex_response
9 {
10 const HTTP_OK = '200 OK';
11 const HTTP_PARTIAL_CONTENT = '206 Partial Content';
12 const HTTP_MOVED_PERMANENTLY = '301 Moved Permanently';
13 const HTTP_NOT_MODIFIED = '304 Not Modified';
14 const HTTP_MOVED_TEMPORARILY = '307 Temporary Redirect';
15 const HTTP_NOT_FOUND = '404 Not Found';
16 const HTTP_FORBIDDEN = '403 Forbidden';
17 const HTTP_UNAUTHORIZED = '401 Unauthorized';
18 const HTTP_RANGE_NOT_SATISFIABLE = '416 Range Not Satisfiable';
19 const HTTP_INTERNAL_ERROR = '500 Internal Server Error';
20 const HTTP_SERVICE_UNAVAILABLE = '503 Service Unavailable';
21
22 private static $httpStatus = self::HTTP_OK;
23 private static $sentLastModified = false;
24 private static $sentEtag = false;
25 private static $sentContentType = false;
26 private static $sentCacheControl = false;
27 private static = [];
28 private static $preloadFiles = [];
29
30 31 32 33 34 35 36
37 public static function setStatus($httpStatus)
38 {
39 if (strpos($httpStatus, "\n") !== false) {
40 throw new InvalidArgumentException('Illegal http-status "' . $httpStatus . '", contains newlines');
41 }
42
43 self::$httpStatus = $httpStatus;
44 }
45
46 47 48 49 50
51 public static function getStatus()
52 {
53 return self::$httpStatus;
54 }
55
56 57 58 59 60 61
62 public static function ($name, $value)
63 {
64 self::$additionalHeaders[$name] = $value;
65 }
66
67 private static function ()
68 {
69 foreach (self::$additionalHeaders as $name => $value) {
70 header($name .': ' . $value);
71 }
72 }
73
74 75 76 77 78 79 80
81 public static function preload($file, $type, $mimeType)
82 {
83 self::$preloadFiles[] = [
84 'file' => $file,
85 'type' => $type,
86 'mimeType' => $mimeType,
87 ];
88 }
89
90 private static function ()
91 {
92 foreach (self::$preloadFiles as $preloadFile) {
93 header('Link: <' . $preloadFile['file'] . '>; rel=preload; as=' . $preloadFile['type'] . '; type="' . $preloadFile['mimeType'].'"; crossorigin; nopush', false);
94 }
95 }
96
97 private static function ()
98 {
99
100 foreach (rex_timer::$serverTimings as $label => $durationMs) {
101 $label = preg_replace('{[^!#$%&\'*+-\.\^_`|~\w]}i', '_', $label);
102 header('Server-Timing: '. $label .';dur='. number_format($durationMs, 3, '.', ''), false);
103 }
104 }
105
106 107 108 109 110 111 112 113 114
115 public static function sendRedirect($url)
116 {
117 if (strpos($url, "\n") !== false) {
118 throw new InvalidArgumentException('Illegal redirect url "' . $url . '", contains newlines');
119 }
120
121 self::cleanOutputBuffers();
122 self::sendAdditionalHeaders();
123 self::sendPreloadHeaders();
124 self::sendServerTimingHeaders();
125
126 header('HTTP/1.1 ' . self::$httpStatus);
127 header('Location: ' . $url);
128 exit;
129 }
130
131 132 133 134 135 136 137 138
139 public static function sendFile($file, $contentType, $contentDisposition = 'inline', $filename = null)
140 {
141 self::cleanOutputBuffers();
142
143 if (!file_exists($file)) {
144 header('HTTP/1.1 ' . self::HTTP_NOT_FOUND);
145 exit;
146 }
147
148
149 session_write_close();
150
151 if (!$filename) {
152 $filename = basename($file);
153 }
154
155 self::sendContentType($contentType);
156 header('Content-Disposition: ' . $contentDisposition . '; filename="' . $filename . '"');
157
158 self::sendLastModified(filemtime($file));
159
160 header('HTTP/1.1 ' . self::$httpStatus);
161 if (!self::$sentCacheControl) {
162 self::sendCacheControl('max-age=3600, must-revalidate, proxy-revalidate, private');
163 }
164
165
166 if (!ini_get('zlib.output_compression')) {
167 header('Content-Length: ' . filesize($file));
168 }
169
170 self::sendAdditionalHeaders();
171 self::sendPreloadHeaders();
172 self::sendServerTimingHeaders();
173
174
175 if (PHP_VERSION_ID >= 50600) {
176 header('Accept-Ranges: bytes');
177 $rangeHeader = rex_request::server('HTTP_RANGE', 'string', null);
178 if ($rangeHeader) {
179 try {
180 $filesize = filesize($file);
181 $unitFactory = new \Ramsey\Http\Range\UnitFactory();
182 $ranges = $unitFactory->getUnit(trim($rangeHeader), $filesize)->getRanges();
183 $handle = fopen($file, 'r');
184 if (is_resource($handle)) {
185 foreach ($ranges as $range) {
186 header('HTTP/1.1 ' . self::HTTP_PARTIAL_CONTENT);
187 header('Content-Length: ' . $range->getLength());
188 header('Content-Range: bytes ' . $range->getStart() . '-' . $range->getEnd() . '/' . $filesize);
189
190
191
192 $chunkSize = $range->getLength() > 8192 ? 8192 : $range->getLength();
193
194 fseek($handle, $range->getStart());
195 while (ftell($handle) < $range->getEnd()) {
196 echo fread($handle, $chunkSize);
197 }
198 }
199 fclose($handle);
200 } else {
201
202 header('HTTP/1.1 ' . self::HTTP_INTERNAL_ERROR);
203 }
204 } catch (\Ramsey\Http\Range\Exception\HttpRangeException $exception) {
205 header('HTTP/1.1 ' . self::HTTP_RANGE_NOT_SATISFIABLE);
206 }
207 return;
208 }
209 }
210
211 readfile($file);
212 }
213
214 215 216 217 218 219 220 221 222 223
224 public static function sendResource($content, $contentType = null, $lastModified = null, $etag = null, $contentDisposition = null, $filename = null)
225 {
226 if ($contentDisposition) {
227 header('Content-Disposition: ' . $contentDisposition . '; filename="' . $filename . '"');
228 }
229
230 self::sendCacheControl('max-age=3600, must-revalidate, proxy-revalidate, private');
231 self::sendContent($content, $contentType, $lastModified, $etag);
232 }
233
234 235 236 237 238 239 240 241
242 public static function sendPage($content, $lastModified = null)
243 {
244
245 $content = rex_extension::registerPoint(new rex_extension_point('OUTPUT_FILTER', $content));
246
247 $hasShutdownExtension = rex_extension::isRegistered('RESPONSE_SHUTDOWN');
248 if ($hasShutdownExtension) {
249 header('Connection: close');
250 }
251
252 self::sendContent($content, null, $lastModified);
253
254
255 if ($hasShutdownExtension) {
256
257 session_write_close();
258
259 rex_extension::registerPoint(new rex_extension_point('RESPONSE_SHUTDOWN', $content, [], true));
260 }
261 }
262
263 264 265 266 267 268 269 270
271 public static function sendContent($content, $contentType = null, $lastModified = null, $etag = null)
272 {
273 if (!self::$sentContentType) {
274 self::sendContentType($contentType);
275 }
276 if (!self::$sentCacheControl) {
277 self::sendCacheControl();
278 }
279
280 $environment = rex::isBackend() ? 'backend' : 'frontend';
281
282 if (
283 self::$httpStatus == self::HTTP_OK &&
284
285
286
287 (!empty($_SERVER['HTTP_USER_AGENT']) && (false === strpos($_SERVER['HTTP_USER_AGENT'], 'Safari') || false !== strpos($_SERVER['HTTP_USER_AGENT'], 'Chrome')))
288 ) {
289
290 if (!self::$sentLastModified
291 && (rex::getProperty('use_last_modified') === true || rex::getProperty('use_last_modified') === $environment)
292 ) {
293 self::sendLastModified($lastModified);
294 }
295
296
297 if (!self::$sentEtag
298 && (rex::getProperty('use_etag') === true || rex::getProperty('use_etag') === $environment)
299 ) {
300 self::sendEtag($etag ?: self::md5($content));
301 }
302 }
303
304
305 if (rex::getProperty('use_gzip') === true || rex::getProperty('use_gzip') === $environment) {
306 $content = self::sendGzip($content);
307 }
308
309 self::cleanOutputBuffers();
310
311 header('HTTP/1.1 ' . self::$httpStatus);
312
313
314 header('Content-Length: ' . rex_string::size($content));
315
316 self::sendAdditionalHeaders();
317 self::sendPreloadHeaders();
318 self::sendServerTimingHeaders();
319
320 echo $content;
321
322 if (function_exists('fastcgi_finish_request')) {
323 fastcgi_finish_request();
324 }
325 }
326
327 328 329
330 public static function cleanOutputBuffers()
331 {
332 while (ob_get_level()) {
333 ob_end_clean();
334 }
335 }
336
337 338 339 340 341
342 public static function sendContentType($contentType = null)
343 {
344 header('Content-Type: ' . ($contentType ?: 'text/html; charset=utf-8'));
345 self::$sentContentType = true;
346 }
347
348 349 350
351 public static function sendCacheControl($cacheControl = 'must-revalidate, proxy-revalidate, private, no-cache, max-age=0')
352 {
353 header('Cache-Control: ' . $cacheControl);
354 self::$sentCacheControl = true;
355 }
356
357 358 359 360 361 362 363
364 public static function sendLastModified($lastModified = null)
365 {
366 if (!$lastModified) {
367 $lastModified = time();
368 }
369
370 $lastModified = gmdate('D, d M Y H:i:s T', (float) $lastModified);
371
372
373 header('Last-Modified: ' . $lastModified);
374
375
376
377 if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $_SERVER['HTTP_IF_MODIFIED_SINCE'] == $lastModified) {
378 self::cleanOutputBuffers();
379
380 header('HTTP/1.1 ' . self::HTTP_NOT_MODIFIED);
381 exit;
382 }
383 self::$sentLastModified = true;
384 }
385
386 387 388 389 390 391 392
393 public static function sendEtag($cacheKey)
394 {
395
396 $cacheKey = '"' . $cacheKey . '"';
397
398
399 header('ETag: ' . $cacheKey);
400
401
402
403 if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] == $cacheKey) {
404 self::cleanOutputBuffers();
405
406 header('HTTP/1.1 ' . self::HTTP_NOT_MODIFIED);
407 exit;
408 }
409 self::$sentEtag = true;
410 }
411
412 413 414 415 416 417 418 419 420
421 protected static function sendGzip($content)
422 {
423 $enc = '';
424 $encodings = [];
425 $supportsGzip = false;
426
427
428 if (isset($_SERVER['HTTP_ACCEPT_ENCODING'])) {
429 $encodings = explode(',', strtolower(preg_replace('/\s+/', '', $_SERVER['HTTP_ACCEPT_ENCODING'])));
430 }
431
432 if ((in_array('gzip', $encodings) || in_array('x-gzip', $encodings) || isset($_SERVER['---------------']))
433 && function_exists('ob_gzhandler')
434 && !ini_get('zlib.output_compression')
435 ) {
436 $enc = in_array('x-gzip', $encodings) ? 'x-gzip' : 'gzip';
437 $supportsGzip = true;
438 }
439
440 if ($supportsGzip) {
441 header('Content-Encoding: ' . $enc);
442 $content = gzencode($content, 9, FORCE_GZIP);
443 }
444
445 return $content;
446 }
447
448
449
450 451 452 453 454 455 456 457 458 459 460 461 462 463
464 public static function sendCookie($name, $value, array $options = [])
465 {
466 $expire = isset($options['expires']) ? $options['expires'] : 0;
467 $path = isset($options['path']) ? $options['path'] : '/';
468 $domain = isset($options['domain']) ? $options['domain'] : null;
469 $secure = isset($options['secure']) ? $options['secure'] : false;
470 $httpOnly = isset($options['httponly']) ? $options['httponly'] : true;
471 $sameSite = isset($options['samesite']) ? $options['samesite'] : null;
472 $raw = isset($options['raw']) ? $options['raw'] : false;
473
474
475 if (preg_match("/[=,; \t\r\n\013\014]/", $name)) {
476 throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $name));
477 }
478 if (empty($name)) {
479 throw new \InvalidArgumentException('The cookie name cannot be empty.');
480 }
481
482 if ($expire instanceof \DateTimeInterface) {
483 $expire = $expire->format('U');
484 } elseif (!is_numeric($expire)) {
485 $expire = strtotime($expire);
486 if (false === $expire) {
487 throw new \InvalidArgumentException('The cookie expiration time is not valid.');
488 }
489 }
490
491 $expire = 0 < $expire ? (int) $expire : 0;
492 $maxAge = $expire - time();
493 $maxAge = 0 >= $maxAge ? 0 : $maxAge;
494 $path = empty($path) ? '/' : $path;
495
496 if (null !== $sameSite) {
497 $sameSite = strtolower($sameSite);
498 }
499 if (!in_array($sameSite, ['lax', 'strict', null], true)) {
500 throw new \InvalidArgumentException('The "sameSite" parameter value is not valid.');
501 }
502
503 $str = 'Set-Cookie: '. ($raw ? $name : urlencode($name)).'=';
504 if ('' === (string) $value) {
505 $str .= 'deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0';
506 } else {
507 $str .= $raw ? $value : rawurlencode($value);
508 if (0 !== $expire) {
509 $str .= '; expires='.gmdate('D, d-M-Y H:i:s T', $expire).'; Max-Age='.$maxAge;
510 }
511 }
512 if ($path) {
513 $str .= '; path='.$path;
514 }
515 if ($domain) {
516 $str .= '; domain='.$domain;
517 }
518 if ($secure) {
519 $str .= '; secure';
520 }
521 if ($httpOnly) {
522 $str .= '; httponly';
523 }
524 if ($sameSite) {
525 $str .= '; samesite='.$sameSite;
526 }
527
528 header($str, false);
529 }
530
531 532 533 534 535 536 537 538 539
540 private static function md5($content)
541 {
542 return md5(preg_replace('@<!--DYN-->.*<!--/DYN-->@U', '', $content));
543 }
544
545 public static function enforceHttps()
546 {
547 if (!rex_request::isHttps()) {
548 self::setStatus(self::HTTP_MOVED_PERMANENTLY);
549 self::sendRedirect('https://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']);
550 }
551 }
552 }
553