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