Merge branch 'master' into next
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / util / HeaderUtil.class.php
1 <?php
2 declare(strict_types=1);
3 namespace wcf\util;
4 use wcf\system\application\ApplicationHandler;
5 use wcf\system\event\EventHandler;
6 use wcf\system\request\RequestHandler;
7 use wcf\system\request\RouteHandler;
8 use wcf\system\WCF;
9
10 /**
11 * Contains header-related functions.
12 *
13 * @author Marcel Werk
14 * @copyright 2001-2018 WoltLab GmbH
15 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
16 * @package WoltLabSuite\Core\Util
17 */
18 final class HeaderUtil {
19 /**
20 * gzip level to user
21 * @var integer
22 */
23 const GZIP_LEVEL = 1;
24
25 /**
26 * gzip compression
27 * @var boolean
28 */
29 protected static $enableGzipCompression = false;
30
31 /**
32 * output HTML
33 * @var string
34 */
35 public static $output = '';
36
37 /**
38 * Alias to php setcookie() function.
39 *
40 * @param string $name
41 * @param string $value
42 * @param integer $expire
43 */
44 public static function setCookie($name, $value = '', $expire = 0) {
45 $application = ApplicationHandler::getInstance()->getActiveApplication();
46 $addDomain = (mb_strpos($application->cookieDomain, '.') === false || StringUtil::endsWith($application->cookieDomain, '.lan') || StringUtil::endsWith($application->cookieDomain, '.local')) ? false : true;
47 $cookieDomain = $application->cookieDomain;
48 if ($addDomain && strpos($cookieDomain, ':') !== false) {
49 $cookieDomain = explode(':', $cookieDomain, 2)[0];
50 }
51
52 @header('Set-Cookie: '.rawurlencode(COOKIE_PREFIX.$name).'='.rawurlencode((string) $value).($expire ? '; expires='.gmdate('D, d-M-Y H:i:s', $expire).' GMT; max-age='.($expire - TIME_NOW) : '').'; path=/'.($addDomain ? '; domain='.$cookieDomain : '').(RouteHandler::secureConnection() ? '; secure' : '').'; HttpOnly', false);
53 }
54
55 /**
56 * Sends the headers of a page.
57 */
58 public static function sendHeaders() {
59 // send content type
60 @header('Content-Type: text/html; charset=UTF-8');
61
62 // send no cache headers
63 if (!PACKAGE_ID || !WCF::getSession()->spiderID) {
64 self::sendNoCacheHeaders();
65 }
66
67 if (HTTP_ENABLE_GZIP && !defined('HTTP_DISABLE_GZIP')) {
68 if (function_exists('gzcompress') && !@ini_get('zlib.output_compression') && !@ini_get('output_handler') && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false) {
69 self::$enableGzipCompression = true;
70
71 if (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'x-gzip') !== false) {
72 @header('Content-Encoding: x-gzip');
73 }
74 else {
75 @header('Content-Encoding: gzip');
76 }
77 }
78 }
79
80 // send Internet Explorer compatibility mode
81 @header('X-UA-Compatible: IE=edge');
82
83 // send X-Frame-Options
84 if (HTTP_SEND_X_FRAME_OPTIONS) {
85 @header('X-Frame-Options: SAMEORIGIN');
86 }
87
88 ob_start([self::class, 'parseOutput']);
89 }
90
91 /**
92 * Sends no cache headers.
93 */
94 public static function sendNoCacheHeaders() {
95 @header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
96 @header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
97 @header('Cache-Control: max-age=0, no-cache, no-store, must-revalidate');
98 @header('Pragma: no-cache');
99 }
100
101 /**
102 * Disables gzip compression on runtime in case of an exception. You should not call
103 * this method at all, it exists for exception handling only.
104 */
105 public static function exceptionDisableGzip() {
106 self::$enableGzipCompression = false;
107 }
108
109 /**
110 * Parses the rendered output.
111 *
112 * @param string $output
113 * @return string
114 */
115 public static function parseOutput($output) {
116 self::$output = $output;
117
118 if (!PACKAGE_ID || RequestHandler::getInstance()->isACPRequest()) {
119 // force javascript relocation
120 self::$output = preg_replace('~<script([^>]*)>~', '<script data-relocate="true"\\1>', self::$output);
121 }
122
123 // move script tags to the bottom of the page
124 $javascript = [];
125 self::$output = preg_replace_callback('~(?P<conditionBefore><!--\[IF [^<]+\s*)?<script data-relocate="true"(?P<script>.*?</script>\s*)(?P<conditionAfter><!\[ENDIF]-->\s*)?~s', function($matches) use (&$javascript) {
126 $match = '';
127 if (isset($matches['conditionBefore'])) $match .= $matches['conditionBefore'];
128 $match .= '<script' . $matches['script'];
129 if (isset($matches['conditionAfter'])) $match .= $matches['conditionAfter'];
130
131 $javascript[] = $match;
132 return '';
133 }, self::$output);
134
135 self::$output = str_replace('<!-- JAVASCRIPT_RELOCATE_POSITION -->', implode("\n", $javascript), self::$output);
136
137 // 3rd party plugins may differ the actual output before it is sent to the browser
138 // please be aware, that $eventObj is not available here due to this being a static
139 // class. Use HeaderUtil::$output to modify it.
140 if (!defined('NO_IMPORTS')) EventHandler::getInstance()->fireAction(self::class, 'parseOutput');
141
142 // gzip compression
143 if (self::$enableGzipCompression) {
144 $size = strlen(self::$output);
145 $crc = crc32(self::$output);
146
147 $newOutput = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff";
148 $newOutput .= substr(gzcompress(self::$output, self::GZIP_LEVEL), 2, -4);
149 $newOutput .= pack('V', $crc);
150 $newOutput .= pack('V', $size);
151
152 self::$output = $newOutput;
153 }
154
155 return self::$output;
156 }
157
158 /**
159 * Redirects the user agent to given location.
160 *
161 * @param string $location
162 * @param boolean $sendStatusCode
163 * @param boolean $temporaryRedirect
164 */
165 public static function redirect($location, $sendStatusCode = false, $temporaryRedirect = true) {
166 if ($sendStatusCode) {
167 if ($temporaryRedirect) @header('HTTP/1.1 307 Temporary Redirect');
168 else @header('HTTP/1.1 301 Moved Permanently');
169 }
170
171 header('Location: '.$location);
172 }
173
174 /**
175 * Does a delayed redirect.
176 *
177 * @param string $location
178 * @param string $message
179 * @param integer $delay
180 * @param string $status
181 */
182 public static function delayedRedirect($location, $message, $delay = 5, $status = 'success') {
183 WCF::getTPL()->assign([
184 'url' => $location,
185 'message' => $message,
186 'wait' => $delay,
187 'templateName' => 'redirect',
188 'templateNameApplication' => 'wcf',
189 'status' => $status
190 ]);
191 WCF::getTPL()->display('redirect');
192 }
193
194 /**
195 * Forbid creation of HeaderUtil objects.
196 */
197 private function __construct() {
198 // does nothing
199 }
200 }