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