Implement the (parent)?Page* values in SessionHandler::__get()
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / WCF.class.php
CommitLineData
d335fa16
AE
1<?php
2namespace wcf\system;
3use wcf\data\application\Application;
4use wcf\data\option\OptionEditor;
5use wcf\data\package\Package;
6use wcf\data\package\PackageCache;
7use wcf\data\package\PackageEditor;
12a2ff01
AE
8use wcf\data\page\Page;
9use wcf\data\page\PageCache;
5346b183 10use wcf\page\CmsPage;
d335fa16 11use wcf\system\application\ApplicationHandler;
f341086b 12use wcf\system\application\IApplication;
55b402a0 13use wcf\system\box\BoxHandler;
d335fa16
AE
14use wcf\system\cache\builder\CoreObjectCacheBuilder;
15use wcf\system\cache\builder\PackageUpdateCacheBuilder;
16use wcf\system\cronjob\CronjobScheduler;
157054c9 17use wcf\system\database\MySQLDatabase;
d335fa16
AE
18use wcf\system\event\EventHandler;
19use wcf\system\exception\AJAXException;
b25ad8f3 20use wcf\system\exception\ErrorException;
d335fa16
AE
21use wcf\system\exception\IPrintableException;
22use wcf\system\exception\NamedUserException;
7b9ff46b 23use wcf\system\exception\ParentClassException;
d335fa16
AE
24use wcf\system\exception\PermissionDeniedException;
25use wcf\system\exception\SystemException;
26use wcf\system\language\LanguageFactory;
27use wcf\system\package\PackageInstallationDispatcher;
11117cd5 28use wcf\system\registry\RegistryHandler;
12a2ff01 29use wcf\system\request\Request;
a80873d5 30use wcf\system\request\RequestHandler;
d335fa16
AE
31use wcf\system\session\SessionFactory;
32use wcf\system\session\SessionHandler;
33use wcf\system\style\StyleHandler;
15621401 34use wcf\system\template\EmailTemplateEngine;
d335fa16
AE
35use wcf\system\template\TemplateEngine;
36use wcf\system\user\storage\UserStorageHandler;
66d9b64b 37use wcf\util\DirectoryUtil;
d335fa16 38use wcf\util\FileUtil;
8418bed7 39use wcf\util\HeaderUtil;
d335fa16 40use wcf\util\StringUtil;
dc0015ab 41use wcf\util\UserUtil;
d335fa16
AE
42
43// try to set a time-limit to infinite
44@set_time_limit(0);
45
46// fix timezone warning issue
47if (!@ini_get('date.timezone')) {
48 @date_default_timezone_set('Europe/London');
49}
50
359841c3 51// define current woltlab suite version
766f40fe 52define('WCF_VERSION', '5.3.0 RC 1');
d335fa16 53
89484ba0 54// define current API version
0e8af3ef 55// @deprecated 5.2
04acbf0b 56define('WSC_API_VERSION', 2019);
89484ba0 57
d335fa16
AE
58// define current unix timestamp
59define('TIME_NOW', time());
60
61// wcf imports
62if (!defined('NO_IMPORTS')) {
63 require_once(WCF_DIR.'lib/core.functions.php');
d11a8c9e 64 require_once(WCF_DIR.'lib/system/api/autoload.php');
d335fa16
AE
65}
66
67/**
e71525e4 68 * WCF is the central class for the WoltLab Suite Core.
d335fa16
AE
69 * It holds the database connection, access to template and language engine.
70 *
71 * @author Marcel Werk
7b7b9764 72 * @copyright 2001-2019 WoltLab GmbH
d335fa16 73 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
e71525e4 74 * @package WoltLabSuite\Core\System
d335fa16
AE
75 */
76class WCF {
89484ba0
AE
77 /**
78 * list of supported legacy API versions
79 * @var integer[]
0e8af3ef 80 * @deprecated 5.2
89484ba0 81 */
04acbf0b 82 private static $supportedLegacyApiVersions = [2017, 2018];
89484ba0 83
d335fa16
AE
84 /**
85 * list of currently loaded applications
893aace3 86 * @var Application[]
d335fa16 87 */
058cbd6a 88 protected static $applications = [];
d335fa16
AE
89
90 /**
91 * list of currently loaded application objects
7a23a706 92 * @var IApplication[]
d335fa16 93 */
058cbd6a 94 protected static $applicationObjects = [];
d335fa16
AE
95
96 /**
97 * list of autoload directories
98 * @var array
99 */
058cbd6a 100 protected static $autoloadDirectories = [];
d335fa16
AE
101
102 /**
103 * list of unique instances of each core object
7a23a706 104 * @var SingletonFactory[]
d335fa16 105 */
058cbd6a 106 protected static $coreObject = [];
d335fa16
AE
107
108 /**
109 * list of cached core objects
7a23a706 110 * @var string[]
d335fa16 111 */
058cbd6a 112 protected static $coreObjectCache = [];
d335fa16
AE
113
114 /**
115 * database object
4e25add7 116 * @var MySQLDatabase
d335fa16 117 */
85099417 118 protected static $dbObj;
d335fa16
AE
119
120 /**
121 * language object
a59497a6 122 * @var \wcf\data\language\Language
d335fa16 123 */
85099417 124 protected static $languageObj;
d335fa16
AE
125
126 /**
127 * overrides disabled debug mode
128 * @var boolean
129 */
130 protected static $overrideDebugMode = false;
131
132 /**
133 * session object
4e25add7 134 * @var SessionHandler
d335fa16 135 */
85099417 136 protected static $sessionObj;
d335fa16
AE
137
138 /**
139 * template object
4e25add7 140 * @var TemplateEngine
d335fa16 141 */
85099417 142 protected static $tplObj;
d335fa16
AE
143
144 /**
145 * true if Zend Opcache is loaded and enabled
146 * @var boolean
147 */
85099417 148 protected static $zendOpcacheEnabled;
d335fa16 149
ab84d9ca
AE
150 /**
151 * force logout during destructor call
152 * @var boolean
153 */
154 protected static $forceLogout = false;
155
d335fa16
AE
156 /**
157 * Calls all init functions of the WCF class.
158 */
159 public function __construct() {
160 // add autoload directory
161 self::$autoloadDirectories['wcf'] = WCF_DIR . 'lib/';
162
163 // define tmp directory
164 if (!defined('TMP_DIR')) define('TMP_DIR', FileUtil::getTempFolder());
165
166 // start initialization
d335fa16
AE
167 $this->initDB();
168 $this->loadOptions();
169 $this->initSession();
170 $this->initLanguage();
171 $this->initTPL();
172 $this->initCronjobs();
173 $this->initCoreObjects();
174 $this->initApplications();
175 $this->initBlacklist();
176
177 EventHandler::getInstance()->fireAction($this, 'initialized');
178 }
179
180 /**
3ba197fa
TD
181 * Flushes the output, closes the session, performs background tasks and more.
182 *
183 * You *must* not create output in here under normal circumstances, as it might get eaten
184 * when gzip is enabled.
d335fa16
AE
185 */
186 public static function destruct() {
187 try {
188 // database has to be initialized
189 if (!is_object(self::$dbObj)) return;
190
3ba197fa
TD
191 $debug = self::debugModeIsEnabled(true);
192 if (!$debug) {
193 // flush output
194 if (ob_get_level()) ob_end_flush();
d335fa16 195 flush();
3ba197fa
TD
196
197 // close connection if using FPM
198 if (function_exists('fastcgi_finish_request')) fastcgi_finish_request();
d335fa16
AE
199 }
200
201 // update session
202 if (is_object(self::getSession())) {
ab84d9ca
AE
203 if (self::$forceLogout) {
204 // do logout
1eb97b3a 205 self::getSession()->delete();
ab84d9ca
AE
206 }
207 else {
208 self::getSession()->update();
209 }
d335fa16
AE
210 }
211
11117cd5
AE
212 // execute shutdown actions of storage handlers
213 RegistryHandler::getInstance()->shutdown();
d335fa16
AE
214 UserStorageHandler::getInstance()->shutdown();
215 }
216 catch (\Exception $exception) {
217 die("<pre>WCF::destruct() Unhandled exception: ".$exception->getMessage()."\n\n".$exception->getTraceAsString());
218 }
219 }
220
d335fa16
AE
221 /**
222 * Returns the database object.
223 *
224 * @return \wcf\system\database\Database
225 */
226 public static final function getDB() {
227 return self::$dbObj;
228 }
229
230 /**
231 * Returns the session object.
232 *
4e25add7 233 * @return SessionHandler
d335fa16
AE
234 */
235 public static final function getSession() {
236 return self::$sessionObj;
237 }
238
239 /**
240 * Returns the user object.
241 *
242 * @return \wcf\data\user\User
243 */
244 public static final function getUser() {
245 return self::getSession()->getUser();
246 }
247
248 /**
249 * Returns the language object.
250 *
251 * @return \wcf\data\language\Language
252 */
253 public static final function getLanguage() {
254 return self::$languageObj;
255 }
256
257 /**
258 * Returns the template object.
259 *
4e25add7 260 * @return TemplateEngine
d335fa16
AE
261 */
262 public static final function getTPL() {
263 return self::$tplObj;
264 }
265
266 /**
267 * Calls the show method on the given exception.
268 *
269 * @param \Exception $e
270 */
e09c7044 271 public static final function handleException($e) {
cbf81afa
AE
272 // backwards compatibility
273 if ($e instanceof IPrintableException) {
274 $e->show();
275 exit;
276 }
277
f8434d25 278 if (ob_get_level()) {
1615fc2e 279 // discard any output generated before the exception occurred, prevents exception
f8434d25 280 // being hidden inside HTML elements and therefore not visible in browser output
f46ac60e
AE
281 //
282 // ob_get_level() can return values > 1, if the PHP setting `output_buffering` is on
283 while (ob_get_level()) ob_end_clean();
5f2c3ab4 284
fef5bd1f
AE
285 // Some webservers are broken and will apply gzip encoding at all cost, but they fail
286 // to set a proper `Content-Encoding` HTTP header and mess things up even more.
287 // Especially the `identity` value appears to be unrecognized by some of them, hence
288 // we'll just gzip the output of the exception to prevent them from tampering.
289 // This part is copied from `HeaderUtil` in order to isolate the exception handler!
4d7a0b73 290 if (defined('HTTP_ENABLE_GZIP') && HTTP_ENABLE_GZIP && !defined('HTTP_DISABLE_GZIP')) {
ac8615e4
LK
291 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) {
292 if (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'x-gzip') !== false) {
fef5bd1f
AE
293 @header('Content-Encoding: x-gzip');
294 }
295 else {
296 @header('Content-Encoding: gzip');
297 }
298
299 ob_start(function($output) {
300 $size = strlen($output);
301 $crc = crc32($output);
302
303 $newOutput = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff";
304 $newOutput .= substr(gzcompress($output, 1), 2, -4);
305 $newOutput .= pack('V', $crc);
306 $newOutput .= pack('V', $size);
307
308 return $newOutput;
309 });
310 }
311 }
f8434d25
AE
312 }
313
5bc5a7e6 314 @header('HTTP/1.1 503 Service Unavailable');
d335fa16 315 try {
5bc5a7e6 316 \wcf\functions\exception\printThrowable($e);
d335fa16 317 }
5bc5a7e6
TD
318 catch (\Throwable $e2) {
319 echo "<pre>An Exception was thrown while handling an Exception:\n\n";
6b7d6653 320 echo preg_replace('/Database->__construct\(.*\)/', 'Database->__construct(...)', $e2);
5bc5a7e6 321 echo "\n\nwas thrown while:\n\n";
6b7d6653 322 echo preg_replace('/Database->__construct\(.*\)/', 'Database->__construct(...)', $e);
5bc5a7e6
TD
323 echo "\n\nwas handled.</pre>";
324 exit;
e09c7044 325 }
d335fa16
AE
326 }
327
328 /**
b25ad8f3 329 * Turns PHP errors into an ErrorException.
d335fa16 330 *
ac52543a 331 * @param integer $severity
d335fa16 332 * @param string $message
ac52543a
MS
333 * @param string $file
334 * @param integer $line
2b770bdd 335 * @throws ErrorException
d335fa16 336 */
b25ad8f3 337 public static final function handleError($severity, $message, $file, $line) {
b2aa772d 338 // this is necessary for the shut-up operator
0267fa9a 339 if (!(error_reporting() & $severity)) return;
b25ad8f3
TD
340
341 throw new ErrorException($message, 0, $severity, $file, $line);
d335fa16
AE
342 }
343
344 /**
345 * Loads the database configuration and creates a new connection to the database.
346 */
347 protected function initDB() {
348 // get configuration
349 $dbHost = $dbUser = $dbPassword = $dbName = '';
350 $dbPort = 0;
20e541d6 351 $defaultDriverOptions = [];
d335fa16
AE
352 require(WCF_DIR.'config.inc.php');
353
354 // create database connection
20e541d6 355 self::$dbObj = new MySQLDatabase($dbHost, $dbUser, $dbPassword, $dbName, $dbPort, false, false, $defaultDriverOptions);
d335fa16
AE
356 }
357
358 /**
359 * Loads the options file, automatically created if not exists.
360 */
361 protected function loadOptions() {
7b0c5bf1 362 // The attachment module is always enabled since 5.2.
85099417
AE
363 // https://github.com/WoltLab/WCF/issues/2531
364 define('MODULE_ATTACHMENT', 1);
365
7b0c5bf1
AE
366 // Users cannot react to their own content since 5.2.
367 // https://github.com/WoltLab/WCF/issues/2975
368 define('LIKE_ALLOW_FOR_OWN_CONTENT', 0);
369 define('LIKE_ENABLE_DISLIKE', 0);
370
86eabb3e 371 // Thumbnails for attachments are already enabled since 5.3.
2cf67fd4 372 // https://github.com/WoltLab/WCF/pull/3444
86eabb3e
AE
373 define('ATTACHMENT_ENABLE_THUMBNAILS', 1);
374
e6fa5c10
MS
375 // User markings are always applied in sidebars since 5.3.
376 // https://github.com/WoltLab/WCF/issues/3330
377 define('MESSAGE_SIDEBAR_ENABLE_USER_ONLINE_MARKING', 1);
378
3e842842
TD
379 // Password strength configuration is deprecated since 5.3.
380 define('REGISTER_ENABLE_PASSWORD_SECURITY_CHECK', 0);
381 define('REGISTER_PASSWORD_MIN_LENGTH', 0);
382 define('REGISTER_PASSWORD_MUST_CONTAIN_LOWER_CASE', 8);
383 define('REGISTER_PASSWORD_MUST_CONTAIN_UPPER_CASE', 0);
384 define('REGISTER_PASSWORD_MUST_CONTAIN_DIGIT', 0);
385 define('REGISTER_PASSWORD_MUST_CONTAIN_SPECIAL_CHAR', 0);
88beebcf 386
533948aa
MW
387 // rel=nofollow is always applied to external link since 5.3
388 // https://github.com/WoltLab/WCF/issues/3339
389 define('EXTERNAL_LINK_REL_NOFOLLOW', 1);
3e842842 390
dbaf3d3e
TD
391 // Session validation is removed since 5.4.
392 // https://github.com/WoltLab/WCF/pull/3583
393 define('SESSION_VALIDATE_IP_ADDRESS', 0);
8b0284c9 394 define('SESSION_VALIDATE_USER_AGENT', 0);
dbaf3d3e 395
a1211f27
TD
396 // Virtual sessions no longer exist since 5.4.
397 define('SESSION_ENABLE_VIRTUALIZATION', 1);
398
6096fe15
TD
399 // The session timeout is fully managed since 5.4.
400 define('SESSION_TIMEOUT', 3600);
401
d335fa16
AE
402 $filename = WCF_DIR.'options.inc.php';
403
404 // create options file if doesn't exist
405 if (!file_exists($filename) || filemtime($filename) <= 1) {
406 OptionEditor::rebuild();
407 }
11beb0a0 408 require($filename);
b842faa1
AE
409
410 // check if option file is complete and writable
411 if (PACKAGE_ID) {
412 if (!is_writable($filename)) {
413 FileUtil::makeWritable($filename);
414
415 if (!is_writable($filename)) {
416 throw new SystemException("The option file '" . $filename . "' is not writable.");
417 }
418 }
419
420 // check if a previous write operation was incomplete and force rebuilding
421 if (!defined('WCF_OPTION_INC_PHP_SUCCESS')) {
422 OptionEditor::rebuild();
423
11beb0a0 424 require($filename);
b842faa1 425 }
34c330f8
AE
426
427 if (ENABLE_DEBUG_MODE) {
428 self::$dbObj->enableDebugMode();
429 }
b842faa1 430 }
d335fa16
AE
431 }
432
433 /**
434 * Starts the session system.
435 */
436 protected function initSession() {
f341086b 437 $factory = new SessionFactory();
d335fa16
AE
438 $factory->load();
439
f341086b 440 self::$sessionObj = SessionHandler::getInstance();
d335fa16
AE
441 }
442
443 /**
444 * Initialises the language engine.
445 */
446 protected function initLanguage() {
447 if (isset($_GET['l']) && !self::getUser()->userID) {
448 self::getSession()->setLanguageID(intval($_GET['l']));
449 }
450
451 // set mb settings
452 mb_internal_encoding('UTF-8');
453 if (function_exists('mb_regex_encoding')) mb_regex_encoding('UTF-8');
454 mb_language('uni');
455
456 // get language
f341086b 457 self::$languageObj = LanguageFactory::getInstance()->getUserLanguage(self::getSession()->getLanguageID());
d335fa16
AE
458 }
459
460 /**
461 * Initialises the template engine.
462 */
463 protected function initTPL() {
f341086b 464 self::$tplObj = TemplateEngine::getInstance();
d335fa16
AE
465 self::getTPL()->setLanguageID(self::getLanguage()->languageID);
466 $this->assignDefaultTemplateVariables();
467
468 $this->initStyle();
469 }
470
471 /**
472 * Initializes the user's style.
473 */
474 protected function initStyle() {
475 if (isset($_REQUEST['styleID'])) {
476 self::getSession()->setStyleID(intval($_REQUEST['styleID']));
477 }
478
f341086b 479 $styleHandler = StyleHandler::getInstance();
d11a8c9e 480 $styleHandler->changeStyle(self::getSession()->getStyleID());
d335fa16
AE
481 }
482
483 /**
484 * Executes the blacklist.
485 */
486 protected function initBlacklist() {
fa75ccfb
AE
487 $isAjax = isset($_SERVER['HTTP_X_REQUESTED_WITH']) && ($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest');
488
d335fa16 489 if (defined('BLACKLIST_IP_ADDRESSES') && BLACKLIST_IP_ADDRESSES != '') {
39c7ff4f 490 if (!StringUtil::executeWordFilter(UserUtil::convertIPv6To4(UserUtil::getIpAddress()), BLACKLIST_IP_ADDRESSES)) {
fa75ccfb 491 if ($isAjax) {
a1721f86 492 throw new AJAXException(self::getLanguage()->getDynamicVariable('wcf.ajax.error.permissionDenied'), AJAXException::INSUFFICIENT_PERMISSIONS);
fa75ccfb
AE
493 }
494 else {
495 throw new PermissionDeniedException();
496 }
dc0015ab 497 }
39c7ff4f 498 else if (!StringUtil::executeWordFilter(UserUtil::getIpAddress(), BLACKLIST_IP_ADDRESSES)) {
fa75ccfb 499 if ($isAjax) {
a1721f86 500 throw new AJAXException(self::getLanguage()->getDynamicVariable('wcf.ajax.error.permissionDenied'), AJAXException::INSUFFICIENT_PERMISSIONS);
fa75ccfb
AE
501 }
502 else {
503 throw new PermissionDeniedException();
504 }
d335fa16
AE
505 }
506 }
507 if (defined('BLACKLIST_USER_AGENTS') && BLACKLIST_USER_AGENTS != '') {
ce64787b 508 if (!StringUtil::executeWordFilter(UserUtil::getUserAgent(), BLACKLIST_USER_AGENTS)) {
fa75ccfb 509 if ($isAjax) {
a1721f86 510 throw new AJAXException(self::getLanguage()->getDynamicVariable('wcf.ajax.error.permissionDenied'), AJAXException::INSUFFICIENT_PERMISSIONS);
fa75ccfb
AE
511 }
512 else {
513 throw new PermissionDeniedException();
514 }
d335fa16
AE
515 }
516 }
517 if (defined('BLACKLIST_HOSTNAMES') && BLACKLIST_HOSTNAMES != '') {
39c7ff4f 518 if (!StringUtil::executeWordFilter(@gethostbyaddr(UserUtil::getIpAddress()), BLACKLIST_HOSTNAMES)) {
fa75ccfb 519 if ($isAjax) {
a1721f86 520 throw new AJAXException(self::getLanguage()->getDynamicVariable('wcf.ajax.error.permissionDenied'), AJAXException::INSUFFICIENT_PERMISSIONS);
fa75ccfb
AE
521 }
522 else {
523 throw new PermissionDeniedException();
524 }
d335fa16
AE
525 }
526 }
527
528 // handle banned users
d0ebece3 529 if (self::getUser()->userID && self::getUser()->banned && !self::getUser()->hasOwnerAccess()) {
fa75ccfb 530 if ($isAjax) {
d335fa16
AE
531 throw new AJAXException(self::getLanguage()->getDynamicVariable('wcf.user.error.isBanned'), AJAXException::INSUFFICIENT_PERMISSIONS);
532 }
533 else {
ab84d9ca
AE
534 self::$forceLogout = true;
535
536 // remove cookies
537 if (isset($_COOKIE[COOKIE_PREFIX.'userID'])) {
538 HeaderUtil::setCookie('userID', 0);
539 }
540 if (isset($_COOKIE[COOKIE_PREFIX.'password'])) {
541 HeaderUtil::setCookie('password', '');
542 }
543
d335fa16
AE
544 throw new NamedUserException(self::getLanguage()->getDynamicVariable('wcf.user.error.isBanned'));
545 }
546 }
547 }
548
549 /**
550 * Initializes applications.
551 */
552 protected function initApplications() {
553 // step 1) load all applications
058cbd6a 554 $loadedApplications = [];
d335fa16
AE
555
556 // register WCF as application
39abe192 557 self::$applications['wcf'] = ApplicationHandler::getInstance()->getApplicationByID(1);
d335fa16 558
39abe192 559 if (!class_exists(WCFACP::class, false)) {
b2b2c26b 560 static::getTPL()->assign('baseHref', self::$applications['wcf']->getPageURL());
39abe192
AE
561 }
562
d335fa16
AE
563 // start main application
564 $application = ApplicationHandler::getInstance()->getActiveApplication();
39abe192
AE
565 if ($application->packageID != 1) {
566 $loadedApplications[] = $this->loadApplication($application);
567
568 // register primary application
569 $abbreviation = ApplicationHandler::getInstance()->getAbbreviation($application->packageID);
570 self::$applications[$abbreviation] = $application;
571 }
d335fa16
AE
572
573 // start dependent applications
574 $applications = ApplicationHandler::getInstance()->getDependentApplications();
575 foreach ($applications as $application) {
39abe192
AE
576 if ($application->packageID == 1) {
577 // ignore WCF
578 continue;
579 }
f4eafe37
AE
580 else if ($application->isTainted) {
581 // ignore apps flagged for uninstallation
582 continue;
583 }
39abe192 584
d335fa16
AE
585 $loadedApplications[] = $this->loadApplication($application, true);
586 }
587
588 // step 2) run each application
589 if (!class_exists('wcf\system\WCFACP', false)) {
f341086b 590 /** @var IApplication $application */
d335fa16
AE
591 foreach ($loadedApplications as $application) {
592 $application->__run();
593 }
594
96bd8977
TD
595 /** @deprecated The below variable is deprecated. */
596 self::getTPL()->assign('__sessionKeepAlive', 59 * 60);
d335fa16
AE
597 }
598 }
599
600 /**
601 * Loads an application.
602 *
2b770bdd
MS
603 * @param Application $application
604 * @param boolean $isDependentApplication
605 * @return IApplication
606 * @throws SystemException
d335fa16
AE
607 */
608 protected function loadApplication(Application $application, $isDependentApplication = false) {
d335fa16
AE
609 $package = PackageCache::getInstance()->getPackage($application->packageID);
610 // package cache might be outdated
611 if ($package === null) {
612 $package = new Package($application->packageID);
613
614 // package cache is outdated, discard cache
615 if ($package->packageID) {
616 PackageEditor::resetCache();
617 }
618 else {
619 // package id is invalid
620 throw new SystemException("application identified by package id '".$application->packageID."' is unknown");
621 }
622 }
e54191ea 623
d335fa16
AE
624 $abbreviation = ApplicationHandler::getInstance()->getAbbreviation($application->packageID);
625 $packageDir = FileUtil::getRealPath(WCF_DIR.$package->packageDir);
626 self::$autoloadDirectories[$abbreviation] = $packageDir . 'lib/';
627
628 $className = $abbreviation.'\system\\'.strtoupper($abbreviation).'Core';
59162cf3
AE
629
630 // class was not found, possibly the app was moved, but `packageDir` has not been adjusted
631 if (!class_exists($className)) {
632 // check if both the Core and the app are on the same domain
633 $coreApp = ApplicationHandler::getInstance()->getApplicationByID(1);
634 if ($coreApp->domainName === $application->domainName) {
635 // resolve the relative path and use it to construct the autoload directory
636 $relativePath = FileUtil::getRelativePath($coreApp->domainPath, $application->domainPath);
637 if ($relativePath !== './') {
638 $packageDir = FileUtil::getRealPath(WCF_DIR.$relativePath);
639 self::$autoloadDirectories[$abbreviation] = $packageDir . 'lib/';
640
641 if (class_exists($className)) {
642 // the class can now be found, update the `packageDir` value
643 (new PackageEditor($package))->update(['packageDir' => $relativePath]);
644 }
645 }
646 }
647 }
648
157054c9 649 if (class_exists($className) && is_subclass_of($className, IApplication::class)) {
d335fa16
AE
650 // include config file
651 $configPath = $packageDir . PackageInstallationDispatcher::CONFIG_FILE;
b842faa1
AE
652 if (!file_exists($configPath)) {
653 Package::writeConfigFile($package->packageID);
654 }
655
d335fa16
AE
656 if (file_exists($configPath)) {
657 require_once($configPath);
658 }
659 else {
660 throw new SystemException('Unable to load configuration for '.$package->package);
661 }
662
663 // register template path if not within ACP
664 if (!class_exists('wcf\system\WCFACP', false)) {
665 // add template path and abbreviation
b2b2c26b 666 static::getTPL()->addApplication($abbreviation, $packageDir . 'templates/');
d335fa16 667 }
15621401 668 EmailTemplateEngine::getInstance()->addApplication($abbreviation, $packageDir . 'templates/');
d335fa16
AE
669
670 // init application and assign it as template variable
058cbd6a 671 self::$applicationObjects[$application->packageID] = call_user_func([$className, 'getInstance']);
b2b2c26b 672 static::getTPL()->assign('__'.$abbreviation, self::$applicationObjects[$application->packageID]);
15621401 673 EmailTemplateEngine::getInstance()->assign('__'.$abbreviation, self::$applicationObjects[$application->packageID]);
d335fa16
AE
674 }
675 else {
676 unset(self::$autoloadDirectories[$abbreviation]);
157054c9 677 throw new SystemException("Unable to run '".$package->package."', '".$className."' is missing or does not implement '".IApplication::class."'.");
d335fa16
AE
678 }
679
680 // register template path in ACP
681 if (class_exists('wcf\system\WCFACP', false)) {
b2b2c26b 682 static::getTPL()->addApplication($abbreviation, $packageDir . 'acp/templates/');
d335fa16
AE
683 }
684 else if (!$isDependentApplication) {
685 // assign base tag
b2b2c26b 686 static::getTPL()->assign('baseHref', $application->getPageURL());
d335fa16
AE
687 }
688
689 // register application
690 self::$applications[$abbreviation] = $application;
691
692 return self::$applicationObjects[$application->packageID];
693 }
694
695 /**
696 * Returns the corresponding application object. Does not support the 'wcf' pseudo application.
697 *
4e25add7
MS
698 * @param Application $application
699 * @return IApplication
d335fa16
AE
700 */
701 public static function getApplicationObject(Application $application) {
702 if (isset(self::$applicationObjects[$application->packageID])) {
703 return self::$applicationObjects[$application->packageID];
704 }
705
706 return null;
707 }
708
dbf41d8a
AE
709 /**
710 * Returns the invoked application.
711 *
712 * @return Application
8e152e07 713 * @since 3.1
dbf41d8a
AE
714 */
715 public static function getActiveApplication() {
716 return ApplicationHandler::getInstance()->getActiveApplication();
717 }
718
d335fa16
AE
719 /**
720 * Loads an application on runtime, do not use this outside the package installation.
721 *
722 * @param integer $packageID
723 */
724 public static function loadRuntimeApplication($packageID) {
725 $package = new Package($packageID);
726 $application = new Application($packageID);
727
728 $abbreviation = Package::getAbbreviation($package->package);
729 $packageDir = FileUtil::getRealPath(WCF_DIR.$package->packageDir);
730 self::$autoloadDirectories[$abbreviation] = $packageDir . 'lib/';
731 self::$applications[$abbreviation] = $application;
732 self::getTPL()->addApplication($abbreviation, $packageDir . 'acp/templates/');
733 }
734
735 /**
736 * Initializes core object cache.
737 */
738 protected function initCoreObjects() {
739 // ignore core objects if installing WCF
740 if (PACKAGE_ID == 0) {
741 return;
742 }
743
744 self::$coreObjectCache = CoreObjectCacheBuilder::getInstance()->getData();
745 }
746
747 /**
748 * Assigns some default variables to the template engine.
749 */
750 protected function assignDefaultTemplateVariables() {
50b7b362
MS
751 $wcf = $this;
752
753 if (ENABLE_ENTERPRISE_MODE) {
754 $wcf = new TemplateScriptingCore($wcf);
755 }
756
e2e8c155 757 self::getTPL()->registerPrefilter(['event', 'hascontent', 'lang', 'jslang']);
058cbd6a 758 self::getTPL()->assign([
50b7b362 759 '__wcf' => $wcf,
e71525e4 760 '__wcfVersion' => LAST_UPDATE_TIME // @deprecated 2.1, use LAST_UPDATE_TIME directly
058cbd6a 761 ]);
54de7a5f
TD
762
763 $isAjax = isset($_SERVER['HTTP_X_REQUESTED_WITH']) && ($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest');
764 // Execute background queue in this request, if it was requested and AJAX isn't used.
765 if (!$isAjax) {
766 if (self::getSession()->getVar('forceBackgroundQueuePerform')) {
767 self::getTPL()->assign([
768 'forceBackgroundQueuePerform' => true
769 ]);
770
771 self::getSession()->unregister('forceBackgroundQueuePerform');
772 }
773 }
774
e2e8c155 775 EmailTemplateEngine::getInstance()->registerPrefilter(['event', 'hascontent', 'lang', 'jslang']);
15621401 776 EmailTemplateEngine::getInstance()->assign([
50b7b362 777 '__wcf' => $wcf
15621401 778 ]);
d335fa16
AE
779 }
780
781 /**
782 * Wrapper for the getter methods of this class.
783 *
784 * @param string $name
785 * @return mixed value
2b770bdd 786 * @throws SystemException
d335fa16
AE
787 */
788 public function __get($name) {
789 $method = 'get'.ucfirst($name);
790 if (method_exists($this, $method)) {
791 return $this->$method();
792 }
793
794 throw new SystemException("method '".$method."' does not exist in class WCF");
795 }
796
487db634
MW
797 /**
798 * Returns true if current application (WCF) is treated as active and was invoked directly.
799 *
800 * @return boolean
801 */
802 public function isActiveApplication() {
803 return (ApplicationHandler::getInstance()->getActiveApplication()->packageID == 1);
804 }
805
d335fa16
AE
806 /**
807 * Changes the active language.
808 *
809 * @param integer $languageID
810 */
811 public static final function setLanguage($languageID) {
5b8eae2c
MW
812 if (!$languageID || LanguageFactory::getInstance()->getLanguage($languageID) === null) {
813 $languageID = LanguageFactory::getInstance()->getDefaultLanguageID();
814 }
2d90d60f 815
d335fa16 816 self::$languageObj = LanguageFactory::getInstance()->getLanguage($languageID);
b372bd53
AE
817
818 // the template engine may not be available yet, usually happens when
819 // changing the user (and thus the language id) during session init
820 if (self::$tplObj !== null) {
821 self::getTPL()->setLanguageID(self::getLanguage()->languageID);
822 EmailTemplateEngine::getInstance()->setLanguageID(self::getLanguage()->languageID);
823 }
d335fa16
AE
824 }
825
826 /**
827 * Includes the required util or exception classes automatically.
828 *
829 * @param string $className
830 * @see spl_autoload_register()
831 */
832 public static final function autoload($className) {
833 $namespaces = explode('\\', $className);
834 if (count($namespaces) > 1) {
835 $applicationPrefix = array_shift($namespaces);
836 if ($applicationPrefix === '') {
837 $applicationPrefix = array_shift($namespaces);
838 }
839 if (isset(self::$autoloadDirectories[$applicationPrefix])) {
840 $classPath = self::$autoloadDirectories[$applicationPrefix] . implode('/', $namespaces) . '.class.php';
6f609fec
AE
841
842 // PHP will implicitly check if the file exists when including it, which means that we can save a
843 // redundant syscall/fs access by not checking for existence ourselves. Do not use require_once()!
844 @include_once($classPath);
d335fa16
AE
845 }
846 }
847 }
848
849 /**
0fcfe5f6 850 * @inheritDoc
d335fa16
AE
851 */
852 public final function __call($name, array $arguments) {
853 // bug fix to avoid php crash, see http://bugs.php.net/bug.php?id=55020
854 if (!method_exists($this, $name)) {
855 return self::__callStatic($name, $arguments);
856 }
857
9b31ab91 858 throw new \BadMethodCallException("Call to undefined method WCF::{$name}().");
d335fa16
AE
859 }
860
861 /**
862 * Returns dynamically loaded core objects.
863 *
864 * @param string $name
865 * @param array $arguments
71952a87 866 * @return object
2b770bdd 867 * @throws SystemException
d335fa16
AE
868 */
869 public static final function __callStatic($name, array $arguments) {
870 $className = preg_replace('~^get~', '', $name);
871
872 if (isset(self::$coreObject[$className])) {
873 return self::$coreObject[$className];
874 }
875
876 $objectName = self::getCoreObject($className);
877 if ($objectName === null) {
878 throw new SystemException("Core object '".$className."' is unknown.");
879 }
880
881 if (class_exists($objectName)) {
63b9817b 882 if (!is_subclass_of($objectName, SingletonFactory::class)) {
7b9ff46b 883 throw new ParentClassException($objectName, SingletonFactory::class);
d335fa16
AE
884 }
885
058cbd6a 886 self::$coreObject[$className] = call_user_func([$objectName, 'getInstance']);
d335fa16
AE
887 return self::$coreObject[$className];
888 }
889 }
890
891 /**
892 * Searches for cached core object definition.
893 *
894 * @param string $className
895 * @return string
896 */
897 protected static final function getCoreObject($className) {
898 if (isset(self::$coreObjectCache[$className])) {
899 return self::$coreObjectCache[$className];
900 }
901
902 return null;
903 }
904
905 /**
906 * Returns true if the debug mode is enabled, otherwise false.
907 *
908 * @param boolean $ignoreACP
909 * @return boolean
910 */
911 public static function debugModeIsEnabled($ignoreACP = false) {
912 // ACP override
913 if (!$ignoreACP && self::$overrideDebugMode) {
914 return true;
915 }
916 else if (defined('ENABLE_DEBUG_MODE') && ENABLE_DEBUG_MODE) {
917 return true;
918 }
919
920 return false;
921 }
922
923 /**
924 * Returns true if benchmarking is enabled, otherwise false.
925 *
926 * @return boolean
927 */
928 public static function benchmarkIsEnabled() {
929 // benchmarking is enabled by default
930 if (!defined('ENABLE_BENCHMARK') || ENABLE_BENCHMARK) return true;
931 return false;
932 }
933
934 /**
935 * Returns domain path for given application.
936 *
937 * @param string $abbreviation
938 * @return string
939 */
940 public static function getPath($abbreviation = 'wcf') {
941 // workaround during WCFSetup
942 if (!PACKAGE_ID) {
943 return '../';
944 }
945
946 if (!isset(self::$applications[$abbreviation])) {
947 $abbreviation = 'wcf';
948 }
949
950 return self::$applications[$abbreviation]->getPageURL();
951 }
952
5d1c96bc
AE
953 /**
954 * Returns the domain path for the currently active application,
955 * used to avoid CORS requests.
956 *
957 * @return string
958 */
959 public static function getActivePath() {
960 if (!PACKAGE_ID) {
961 return self::getPath();
962 }
963
12a7c542
AE
964 // We cannot rely on the ApplicationHandler's `getActiveApplication()` because
965 // it uses the requested controller to determine the namespace. However, starting
966 // with WoltLab Suite 5.2, system pages can be virtually assigned to a different
967 // app, resolving against the target app without changing the namespace.
968 return self::getPath(ApplicationHandler::getInstance()->getAbbreviation(PACKAGE_ID));
5d1c96bc
AE
969 }
970
d335fa16
AE
971 /**
972 * Returns a fully qualified anchor for current page.
973 *
974 * @param string $fragment
975 * @return string
976 */
977 public function getAnchor($fragment) {
694f6545 978 return StringUtil::encodeHTML(self::getRequestURI() . '#' . $fragment);
d335fa16
AE
979 }
980
12a2ff01
AE
981 /**
982 * Returns the currently active page or null if unknown.
983 *
b212ba66 984 * @return Page|null
12a2ff01
AE
985 */
986 public static function getActivePage() {
a039ebd2
AE
987 if (self::getActiveRequest() === null) {
988 return null;
989 }
990
5346b183
AE
991 if (self::getActiveRequest()->getClassName() === CmsPage::class) {
992 $metaData = self::getActiveRequest()->getMetaData();
b212ba66
MS
993 if (isset($metaData['cms'])) {
994 return PageCache::getInstance()->getPage($metaData['cms']['pageID']);
995 }
996
997 return null;
5346b183
AE
998 }
999
12a2ff01
AE
1000 return PageCache::getInstance()->getPageByController(self::getActiveRequest()->getClassName());
1001 }
1002
1003 /**
1004 * Returns the currently active request.
1005 *
1006 * @return Request
1007 */
1008 public static function getActiveRequest() {
1009 return RequestHandler::getInstance()->getActiveRequest();
1010 }
1011
d335fa16
AE
1012 /**
1013 * Returns the URI of the current page.
1014 *
1015 * @return string
1016 */
1017 public static function getRequestURI() {
c68f6161 1018 return preg_replace('~^(https?://[^/]+)(?:/.*)?$~', '$1', self::getTPL()->get('baseHref')) . $_SERVER['REQUEST_URI'];
d335fa16
AE
1019 }
1020
1021 /**
1022 * Resets Zend Opcache cache if installed and enabled.
1023 *
1024 * @param string $script
1025 */
1026 public static function resetZendOpcache($script = '') {
1027 if (self::$zendOpcacheEnabled === null) {
1028 self::$zendOpcacheEnabled = false;
1029
1030 if (extension_loaded('Zend Opcache') && @ini_get('opcache.enable')) {
1031 self::$zendOpcacheEnabled = true;
1032 }
1033
1034 }
1035
1036 if (self::$zendOpcacheEnabled) {
1037 if (empty($script)) {
d631fa86 1038 \opcache_reset();
d335fa16
AE
1039 }
1040 else {
d631fa86 1041 \opcache_invalidate($script, true);
d335fa16
AE
1042 }
1043 }
1044 }
1045
1046 /**
1047 * Returns style handler.
1048 *
4e25add7 1049 * @return StyleHandler
d335fa16
AE
1050 */
1051 public function getStyleHandler() {
1052 return StyleHandler::getInstance();
1053 }
1054
55b402a0
MW
1055 /**
1056 * Returns box handler.
1057 *
1058 * @return BoxHandler
e71525e4 1059 * @since 3.0
55b402a0
MW
1060 */
1061 public function getBoxHandler() {
1062 return BoxHandler::getInstance();
1063 }
1064
d335fa16
AE
1065 /**
1066 * Returns number of available updates.
1067 *
1068 * @return integer
1069 */
1070 public function getAvailableUpdates() {
1071 $data = PackageUpdateCacheBuilder::getInstance()->getData();
1072 return $data['updates'];
1073 }
1074
1075 /**
1076 * Returns a 8 character prefix for editor autosave.
1077 *
1078 * @return string
1079 */
1080 public function getAutosavePrefix() {
1081 return substr(sha1(preg_replace('~^https~', 'http', self::getPath())), 0, 8);
1082 }
1083
1084 /**
1085 * Returns the favicon URL or a base64 encoded image.
1086 *
1087 * @return string
1088 */
1089 public function getFavicon() {
1090 $activeApplication = ApplicationHandler::getInstance()->getActiveApplication();
39abe192 1091 $wcf = ApplicationHandler::getInstance()->getWCF();
aef117b5 1092 $favicon = StyleHandler::getInstance()->getStyle()->getRelativeFavicon();
d335fa16 1093
39abe192 1094 if ($activeApplication->domainName !== $wcf->domainName) {
aef117b5
AE
1095 if (file_exists(WCF_DIR.$favicon)) {
1096 $favicon = file_get_contents(WCF_DIR.$favicon);
d335fa16
AE
1097
1098 return 'data:image/x-icon;base64,' . base64_encode($favicon);
1099 }
1100 }
1101
aef117b5 1102 return self::getPath() . $favicon;
d335fa16
AE
1103 }
1104
6e4aa12f
AE
1105 /**
1106 * Returns true if the desktop notifications should be enabled.
1107 *
1108 * @return boolean
1109 */
1110 public function useDesktopNotifications() {
1111 if (!ENABLE_DESKTOP_NOTIFICATIONS) {
1112 return false;
1113 }
1114 else if (ApplicationHandler::getInstance()->isMultiDomainSetup()) {
1115 $application = ApplicationHandler::getInstance()->getApplicationByID(DESKTOP_NOTIFICATION_PACKAGE_ID);
1116 // mismatch, default to Core
1117 if ($application === null) $application = ApplicationHandler::getInstance()->getApplicationByID(1);
1118
1119 $currentApplication = ApplicationHandler::getInstance()->getActiveApplication();
1120 if ($currentApplication->domainName != $application->domainName) {
1121 // different domain
1122 return false;
1123 }
1124 }
1125
1126 return true;
1127 }
1128
a80873d5
AE
1129 /**
1130 * Returns true if currently active request represents the landing page.
1131 *
8ff2cd79 1132 * @return boolean
a80873d5 1133 */
0fdd7f96 1134 public static function isLandingPage() {
a039ebd2
AE
1135 if (self::getActiveRequest() === null) {
1136 return false;
1137 }
1138
1139 return self::getActiveRequest()->isLandingPage();
a80873d5
AE
1140 }
1141
89484ba0
AE
1142 /**
1143 * Returns true if the given API version is currently supported.
1144 *
1145 * @param integer $apiVersion
1146 * @return boolean
0e8af3ef 1147 * @deprecated 5.2
89484ba0
AE
1148 */
1149 public static function isSupportedApiVersion($apiVersion) {
1150 return ($apiVersion == WSC_API_VERSION) || in_array($apiVersion, self::$supportedLegacyApiVersions);
1151 }
1152
1153 /**
1154 * Returns the list of supported legacy API versions.
1155 *
1156 * @return integer[]
0e8af3ef 1157 * @deprecated 5.2
89484ba0
AE
1158 */
1159 public static function getSupportedLegacyApiVersions() {
1160 return self::$supportedLegacyApiVersions;
1161 }
1162
d335fa16
AE
1163 /**
1164 * Initialises the cronjobs.
1165 */
1166 protected function initCronjobs() {
1167 if (PACKAGE_ID) {
63b9817b 1168 self::getTPL()->assign('executeCronjobs', CronjobScheduler::getInstance()->getNextExec() < TIME_NOW && defined('OFFLINE') && !OFFLINE);
d335fa16
AE
1169 }
1170 }
66d9b64b
MS
1171
1172 /**
1173 * Checks recursively that the most important system files of `com.woltlab.wcf` are writable.
1174 *
1175 * @throws \RuntimeException if any relevant file or directory is not writable
1176 */
1177 public static function checkWritability() {
1178 $nonWritablePaths = [];
1179
1180 $nonRecursiveDirectories = [
1181 '',
1182 'acp/',
1183 'tmp/'
1184 ];
1185 foreach ($nonRecursiveDirectories as $directory) {
1186 $path = WCF_DIR . $directory;
1187 if ($path === 'tmp/' && !is_dir($path)) {
1188 continue;
1189 }
1190
1191 if (!is_writable($path)) {
7f754c71 1192 $nonWritablePaths[] = FileUtil::getRelativePath($_SERVER['DOCUMENT_ROOT'], $path);
66d9b64b
MS
1193 continue;
1194 }
1195
1196 DirectoryUtil::getInstance($path, false)->executeCallback(function($file, \SplFileInfo $fileInfo) use ($path, &$nonWritablePaths) {
1197 if ($fileInfo instanceof \DirectoryIterator) {
1198 if (!is_writable($fileInfo->getPath())) {
7f754c71 1199 $nonWritablePaths[] = FileUtil::getRelativePath($_SERVER['DOCUMENT_ROOT'], $fileInfo->getPath());
66d9b64b
MS
1200 }
1201 }
1202 else if (!is_writable($fileInfo->getRealPath())) {
7f754c71 1203 $nonWritablePaths[] = FileUtil::getRelativePath($_SERVER['DOCUMENT_ROOT'], $fileInfo->getPath()) . $fileInfo->getFilename();
66d9b64b
MS
1204 }
1205 });
1206 }
1207
1208 $recursiveDirectories = [
1209 'acp/js/',
1210 'acp/style/',
1211 'acp/templates/',
1212 'acp/uninstall/',
1213 'js/',
1214 'lib/',
1215 'log/',
1216 'style/',
1217 'templates/'
1218 ];
1219 foreach ($recursiveDirectories as $directory) {
1220 $path = WCF_DIR . $directory;
1221
1222 if (!is_writable($path)) {
7f754c71 1223 $nonWritablePaths[] = FileUtil::getRelativePath($_SERVER['DOCUMENT_ROOT'], $path);
66d9b64b
MS
1224 continue;
1225 }
1226
1227 DirectoryUtil::getInstance($path)->executeCallback(function($file, \SplFileInfo $fileInfo) use ($path, &$nonWritablePaths) {
1228 if ($fileInfo instanceof \DirectoryIterator) {
1229 if (!is_writable($fileInfo->getPath())) {
7f754c71 1230 $nonWritablePaths[] = FileUtil::getRelativePath($_SERVER['DOCUMENT_ROOT'], $fileInfo->getPath());
66d9b64b
MS
1231 }
1232 }
1233 else if (!is_writable($fileInfo->getRealPath())) {
7f754c71 1234 $nonWritablePaths[] = FileUtil::getRelativePath($_SERVER['DOCUMENT_ROOT'], $fileInfo->getPath()) . $fileInfo->getFilename();
66d9b64b
MS
1235 }
1236 });
1237 }
1238
1239 if (!empty($nonWritablePaths)) {
1240 $maxPaths = 10;
cf23b193 1241 throw new \RuntimeException('The following paths are not writable: ' . implode(',', array_slice($nonWritablePaths, 0, $maxPaths)) . (count($nonWritablePaths) > $maxPaths ? ',' . StringUtil::HELLIP : ''));
66d9b64b
MS
1242 }
1243 }
d335fa16 1244}