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