Move the check for offline mode into a dedicated method
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / request / RequestHandler.class.php
1 <?php
2
3 namespace wcf\system\request;
4
5 use wcf\system\application\ApplicationHandler;
6 use wcf\system\box\BoxHandler;
7 use wcf\system\exception\AJAXException;
8 use wcf\system\exception\IllegalLinkException;
9 use wcf\system\exception\NamedUserException;
10 use wcf\system\exception\SystemException;
11 use wcf\system\notice\NoticeHandler;
12 use wcf\system\SingletonFactory;
13 use wcf\system\WCF;
14 use wcf\util\FileUtil;
15 use wcf\util\HeaderUtil;
16
17 /**
18 * Handles http requests.
19 *
20 * @author Marcel Werk
21 * @copyright 2001-2020 WoltLab GmbH
22 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
23 * @package WoltLabSuite\Core\System\Request
24 */
25 class RequestHandler extends SingletonFactory
26 {
27 /**
28 * active request object
29 * @var Request
30 */
31 protected $activeRequest;
32
33 /**
34 * true, if current domain mismatch any known domain
35 * @var bool
36 */
37 protected $inRescueMode = false;
38
39 /**
40 * indicates if the request is an acp request
41 * @var bool
42 */
43 protected $isACPRequest = false;
44
45 /**
46 * @inheritDoc
47 */
48 protected function init()
49 {
50 $this->isACPRequest = \class_exists('wcf\system\WCFACP', false);
51 }
52
53 /**
54 * Handles a http request.
55 *
56 * @param string $application
57 * @param bool $isACPRequest
58 * @throws AJAXException
59 * @throws IllegalLinkException
60 * @throws SystemException
61 */
62 public function handle($application = 'wcf', $isACPRequest = false)
63 {
64 try {
65 $this->isACPRequest = $isACPRequest;
66
67 if (!RouteHandler::getInstance()->matches()) {
68 if (ENABLE_DEBUG_MODE) {
69 throw new SystemException("Cannot handle request, no valid route provided.");
70 } else {
71 throw new IllegalLinkException();
72 }
73 }
74
75 // build request
76 $this->buildRequest($application);
77
78 // enforce that certain ACP pages are not available for non-owners in enterprise mode
79 if (
80 $this->isACPRequest()
81 && ENABLE_ENTERPRISE_MODE
82 && \defined($this->getActiveRequest()->getClassName() . '::BLACKLISTED_IN_ENTERPRISE_MODE')
83 && \constant($this->getActiveRequest()->getClassName() . '::BLACKLISTED_IN_ENTERPRISE_MODE')
84 && !WCF::getUser()->hasOwnerAccess()
85 ) {
86 throw new IllegalLinkException();
87 }
88
89 $this->checkOfflineMode();
90
91 // start request
92 $this->getActiveRequest()->execute();
93 } catch (NamedUserException $e) {
94 $e->show();
95
96 exit;
97 }
98 }
99
100 /**
101 * Builds a new request.
102 *
103 * @param string $application
104 * @throws IllegalLinkException
105 * @throws NamedUserException
106 * @throws SystemException
107 */
108 protected function buildRequest($application)
109 {
110 try {
111 $routeData = RouteHandler::getInstance()->getRouteData();
112
113 // handle landing page for frontend requests
114 if (!$this->isACPRequest()) {
115 $this->handleDefaultController($application, $routeData);
116
117 // check if accessing from the wrong domain (e.g. "www." omitted but domain was configured with)
118 if (!\defined('WCF_RUN_MODE') || WCF_RUN_MODE !== 'embedded') {
119 $applicationObject = ApplicationHandler::getInstance()->getApplication($application);
120 if ($applicationObject->domainName != $_SERVER['HTTP_HOST']) {
121 // build URL, e.g. http://example.net/forum/
122 $url = FileUtil::addTrailingSlash(
123 RouteHandler::getProtocol() . $applicationObject->domainName . RouteHandler::getPath()
124 );
125
126 // query string, e.g. ?foo=bar
127 if (!empty($_SERVER['QUERY_STRING'])) {
128 $url .= '?' . $_SERVER['QUERY_STRING'];
129 }
130
131 HeaderUtil::redirect($url, true, false);
132
133 exit;
134 }
135 }
136 } elseif (empty($routeData['controller'])) {
137 $routeData['controller'] = 'index';
138 }
139
140 $controller = $routeData['controller'];
141
142 if (isset($routeData['className'])) {
143 $classData = [
144 'className' => $routeData['className'],
145 'controller' => $routeData['controller'],
146 'pageType' => $routeData['pageType'],
147 ];
148
149 unset($routeData['className']);
150 unset($routeData['controller']);
151 unset($routeData['pageType']);
152 } else {
153 if (
154 $this->isACPRequest()
155 && ($controller === 'login' || $controller === 'index')
156 && $application !== 'wcf'
157 ) {
158 HeaderUtil::redirect(
159 LinkHandler::getInstance()->getLink(\ucfirst($controller)),
160 true,
161 false
162 );
163
164 exit;
165 }
166
167 $classApplication = $application;
168 if (
169 !empty($routeData['isDefaultController'])
170 && !empty($routeData['application'])
171 && $routeData['application'] !== $application
172 ) {
173 $classApplication = $routeData['application'];
174 }
175
176 $classData = ControllerMap::getInstance()->resolve(
177 $classApplication,
178 $controller,
179 $this->isACPRequest(),
180 RouteHandler::getInstance()->isRenamedController()
181 );
182 if (\is_string($classData)) {
183 $this->redirect($routeData, $application, $classData);
184 }
185 }
186
187 // handle CMS page meta data
188 $metaData = ['isDefaultController' => (!empty($routeData['isDefaultController']))];
189 if (isset($routeData['cmsPageID'])) {
190 $metaData['cms'] = [
191 'pageID' => $routeData['cmsPageID'],
192 'languageID' => $routeData['cmsPageLanguageID'],
193 ];
194
195 if (
196 $routeData['cmsPageLanguageID']
197 && $routeData['cmsPageLanguageID'] != WCF::getLanguage()->languageID
198 ) {
199 WCF::setLanguage($routeData['cmsPageLanguageID']);
200 }
201
202 unset($routeData['cmsPageID']);
203 unset($routeData['cmsPageLanguageID']);
204 }
205
206 $this->activeRequest = new Request(
207 $classData['className'],
208 $classData['controller'],
209 $classData['pageType'],
210 $metaData
211 );
212
213 // check if the controller matches an app that has an expired evaluation date
214 $abbreviation = \mb_substr($classData['className'], 0, \mb_strpos($classData['className'], '\\'));
215 if ($abbreviation !== 'wcf') {
216 $applicationObject = ApplicationHandler::getInstance()->getApplication($abbreviation);
217 $endDate = WCF::getApplicationObject($applicationObject)->getEvaluationEndDate();
218 if ($endDate && $endDate < TIME_NOW) {
219 $package = $applicationObject->getPackage();
220
221 $pluginStoreFileID = WCF::getApplicationObject($applicationObject)->getEvaluationPluginStoreID();
222 $isWoltLab = false;
223 if ($pluginStoreFileID === 0 && \strpos($package->package, 'com.woltlab.') === 0) {
224 $isWoltLab = true;
225 }
226
227 throw new NamedUserException(WCF::getLanguage()->getDynamicVariable(
228 'wcf.acp.package.evaluation.expired',
229 [
230 'packageName' => $package->getName(),
231 'pluginStoreFileID' => $pluginStoreFileID,
232 'isWoltLab' => $isWoltLab,
233 ]
234 ));
235 }
236 }
237
238 if (!$this->isACPRequest()) {
239 // determine if current request matches the landing page
240 if (ControllerMap::getInstance()->isLandingPage($classData, $metaData)) {
241 $this->activeRequest->setIsLandingPage();
242 }
243 }
244
245 ApplicationHandler::getInstance()->rebuildActiveApplication();
246 } catch (SystemException $e) {
247 if (
248 \defined('ENABLE_DEBUG_MODE')
249 && ENABLE_DEBUG_MODE
250 && \defined('ENABLE_DEVELOPER_TOOLS')
251 && ENABLE_DEVELOPER_TOOLS
252 ) {
253 throw $e;
254 }
255
256 throw new IllegalLinkException();
257 }
258 }
259
260 protected function checkOfflineMode()
261 {
262 if (!$this->isACPRequest() && \defined('OFFLINE') && OFFLINE) {
263 if (
264 !WCF::getSession()->getPermission('admin.general.canViewPageDuringOfflineMode')
265 && !$this->getActiveRequest()->isAvailableDuringOfflineMode()
266 ) {
267 if (
268 isset($_SERVER['HTTP_X_REQUESTED_WITH'])
269 && ($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest')
270 ) {
271 throw new AJAXException(
272 WCF::getLanguage()->getDynamicVariable('wcf.ajax.error.permissionDenied'),
273 AJAXException::INSUFFICIENT_PERMISSIONS
274 );
275 } else {
276 @\header('HTTP/1.1 503 Service Unavailable');
277 BoxHandler::disablePageLayout();
278 NoticeHandler::disableNotices();
279 WCF::getTPL()->assign([
280 'templateName' => 'offline',
281 'templateNameApplication' => 'wcf',
282 ]);
283 WCF::getTPL()->display('offline');
284 }
285
286 exit;
287 }
288 }
289 }
290
291 /**
292 * Redirects to the actual URL, e.g. controller has been aliased or mistyped (boardlist instead of board-list).
293 *
294 * @param string[] $routeData
295 * @param string $application
296 * @param string $controller
297 */
298 protected function redirect(array $routeData, $application, $controller = null)
299 {
300 $routeData['application'] = $application;
301 if ($controller !== null) {
302 $routeData['controller'] = $controller;
303 }
304
305 // append the remaining query parameters
306 foreach ($_GET as $key => $value) {
307 if (!empty($value) && $key != 'controller') {
308 $routeData[$key] = $value;
309 }
310 }
311
312 $redirectURL = LinkHandler::getInstance()->getLink($routeData['controller'], $routeData);
313 HeaderUtil::redirect($redirectURL, true, false);
314
315 exit;
316 }
317
318 /**
319 * Checks page access for possible mandatory redirects.
320 *
321 * @param string $application
322 * @param string[] $routeData
323 * @throws IllegalLinkException
324 */
325 protected function handleDefaultController($application, array &$routeData)
326 {
327 if (!RouteHandler::getInstance()->isDefaultController()) {
328 return;
329 }
330
331 $data = ControllerMap::getInstance()->lookupDefaultController($application);
332 if ($data === null) {
333 // handle WCF which does not have a default controller
334 throw new IllegalLinkException();
335 } elseif (!empty($data['redirect'])) {
336 // force a redirect
337 HeaderUtil::redirect($data['redirect'], true, false);
338
339 exit;
340 } elseif (!empty($data['application']) && $data['application'] !== $application) {
341 $override = ControllerMap::getInstance()->getApplicationOverride($application, $data['controller']);
342 if ($application !== $override) {
343 HeaderUtil::redirect(
344 LinkHandler::getInstance()->getLink(
345 ControllerMap::getInstance()->resolve(
346 $data['application'],
347 $data['controller'],
348 false
349 )['controller'],
350 ['application' => $data['application']]
351 ),
352 true,
353 true
354 );
355
356 exit;
357 }
358 }
359
360 // copy route data
361 foreach ($data as $key => $value) {
362 $routeData[$key] = $value;
363 }
364
365 $routeData['isDefaultController'] = true;
366 }
367
368 /**
369 * Returns the active request object.
370 *
371 * @return Request
372 */
373 public function getActiveRequest()
374 {
375 return $this->activeRequest;
376 }
377
378 /**
379 * Returns true if the request is an acp request.
380 *
381 * @return bool
382 */
383 public function isACPRequest()
384 {
385 return $this->isACPRequest;
386 }
387
388 /**
389 * Returns true, if current host mismatches any known domain.
390 *
391 * @return bool
392 */
393 public function inRescueMode()
394 {
395 return $this->inRescueMode;
396 }
397 }