Merge branch 'master' into next
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / WCF.class.php
index e365d1c3bd9639ee2e84df00d8044b287629b271..3ab7dc1ac55791383949331b1eece7c1f01bcd72 100644 (file)
@@ -1,31 +1,42 @@
 <?php
+declare(strict_types=1);
 namespace wcf\system;
 use wcf\data\application\Application;
 use wcf\data\option\OptionEditor;
 use wcf\data\package\Package;
 use wcf\data\package\PackageCache;
 use wcf\data\package\PackageEditor;
+use wcf\data\page\Page;
+use wcf\data\page\PageCache;
+use wcf\page\CmsPage;
 use wcf\system\application\ApplicationHandler;
+use wcf\system\application\IApplication;
+use wcf\system\box\BoxHandler;
 use wcf\system\cache\builder\CoreObjectCacheBuilder;
 use wcf\system\cache\builder\PackageUpdateCacheBuilder;
 use wcf\system\cronjob\CronjobScheduler;
+use wcf\system\database\MySQLDatabase;
 use wcf\system\event\EventHandler;
 use wcf\system\exception\AJAXException;
+use wcf\system\exception\ErrorException;
 use wcf\system\exception\IPrintableException;
 use wcf\system\exception\NamedUserException;
+use wcf\system\exception\ParentClassException;
 use wcf\system\exception\PermissionDeniedException;
 use wcf\system\exception\SystemException;
 use wcf\system\language\LanguageFactory;
 use wcf\system\package\PackageInstallationDispatcher;
-use wcf\system\request\RouteHandler;
+use wcf\system\registry\RegistryHandler;
+use wcf\system\request\Request;
+use wcf\system\request\RequestHandler;
 use wcf\system\session\SessionFactory;
 use wcf\system\session\SessionHandler;
 use wcf\system\style\StyleHandler;
+use wcf\system\template\EmailTemplateEngine;
 use wcf\system\template\TemplateEngine;
 use wcf\system\user\storage\UserStorageHandler;
-use wcf\util\ArrayUtil;
-use wcf\util\ClassUtil;
 use wcf\util\FileUtil;
+use wcf\util\HeaderUtil;
 use wcf\util\StringUtil;
 use wcf\util\UserUtil;
 
@@ -37,8 +48,11 @@ if (!@ini_get('date.timezone')) {
        @date_default_timezone_set('Europe/London');
 }
 
-// define current wcf version
-define('WCF_VERSION', '2.1.21 (Typhoon)');
+// define current woltlab suite version
+define('WCF_VERSION', '3.1.2 pl 2');
+
+// define current API version
+define('WSC_API_VERSION', 2018);
 
 // define current unix timestamp
 define('TIME_NOW', time());
@@ -46,53 +60,58 @@ define('TIME_NOW', time());
 // wcf imports
 if (!defined('NO_IMPORTS')) {
        require_once(WCF_DIR.'lib/core.functions.php');
+       require_once(WCF_DIR.'lib/system/api/autoload.php');
 }
 
 /**
- * WCF is the central class for the community framework.
+ * WCF is the central class for the WoltLab Suite Core.
  * It holds the database connection, access to template and language engine.
  * 
  * @author     Marcel Werk
- * @copyright  2001-2016 WoltLab GmbH
+ * @copyright  2001-2018 WoltLab GmbH
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @package    com.woltlab.wcf
- * @subpackage system
- * @category   Community Framework
+ * @package    WoltLabSuite\Core\System
  */
 class WCF {
+       /**
+        * list of supported legacy API versions
+        * @var integer[]
+        */
+       private static $supportedLegacyApiVersions = [2017];
+       
        /**
         * list of currently loaded applications
-        * @var array<\wcf\data\application\Application>
+        * @var Application[]
         */
-       protected static $applications = array();
+       protected static $applications = [];
        
        /**
         * list of currently loaded application objects
-        * @var array<\wcf\system\application\IApplication>
+        * @var IApplication[]
         */
-       protected static $applicationObjects = array();
+       protected static $applicationObjects = [];
        
        /**
         * list of autoload directories
         * @var array
         */
-       protected static $autoloadDirectories = array();
+       protected static $autoloadDirectories = [];
        
        /**
         * list of unique instances of each core object
-        * @var array<\wcf\system\SingletonFactory>
+        * @var SingletonFactory[]
         */
-       protected static $coreObject = array();
+       protected static $coreObject = [];
        
        /**
         * list of cached core objects
-        * @var array<array>
+        * @var string[]
         */
-       protected static $coreObjectCache = array();
+       protected static $coreObjectCache = [];
        
        /**
         * database object
-        * @var \wcf\system\database\Database
+        * @var MySQLDatabase
         */
        protected static $dbObj = null;
        
@@ -110,13 +129,13 @@ class WCF {
        
        /**
         * session object
-        * @var \wcf\system\session\SessionHandler
+        * @var SessionHandler
         */
        protected static $sessionObj = null;
        
        /**
         * template object
-        * @var \wcf\system\template\TemplateEngine
+        * @var TemplateEngine
         */
        protected static $tplObj = null;
        
@@ -126,6 +145,12 @@ class WCF {
         */
        protected static $zendOpcacheEnabled = null;
        
+       /**
+        * force logout during destructor call
+        * @var boolean
+        */
+       protected static $forceLogout = false;
+       
        /**
         * Calls all init functions of the WCF class.
         */
@@ -137,7 +162,6 @@ class WCF {
                if (!defined('TMP_DIR')) define('TMP_DIR', FileUtil::getTempFolder());
                
                // start initialization
-               $this->initMagicQuotes();
                $this->initDB();
                $this->loadOptions();
                $this->initSession();
@@ -152,30 +176,39 @@ class WCF {
        }
        
        /**
-        * Replacement of the "__destruct()" method.
-        * Seems that under specific conditions (windows) the destructor is not called automatically.
-        * So we use the php register_shutdown_function to register an own destructor method.
-        * Flushs the output, closes caches and updates the session.
+        * Flushes the output, closes the session, performs background tasks and more.
+        * 
+        * You *must* not create output in here under normal circumstances, as it might get eaten
+        * when gzip is enabled.
         */
        public static function destruct() {
                try {
                        // database has to be initialized
                        if (!is_object(self::$dbObj)) return;
                        
-                       // flush output
-                       if (ob_get_level() && ini_get('output_handler')) {
-                               ob_flush();
-                       }
-                       else {
+                       $debug = self::debugModeIsEnabled(true);
+                       if (!$debug) {
+                               // flush output
+                               if (ob_get_level()) ob_end_flush();
                                flush();
+                               
+                               // close connection if using FPM
+                               if (function_exists('fastcgi_finish_request')) fastcgi_finish_request();
                        }
                        
                        // update session
                        if (is_object(self::getSession())) {
-                               self::getSession()->update();
+                               if (self::$forceLogout) {
+                                       // do logout
+                                       self::getSession()->delete();
+                               }
+                               else {
+                                       self::getSession()->update();
+                               }
                        }
                        
-                       // execute shutdown actions of user storage handler
+                       // execute shutdown actions of storage handlers
+                       RegistryHandler::getInstance()->shutdown();
                        UserStorageHandler::getInstance()->shutdown();
                }
                catch (\Exception $exception) {
@@ -183,41 +216,6 @@ class WCF {
                }
        }
        
-       /**
-        * Removes slashes in superglobal gpc data arrays if 'magic quotes gpc' is enabled.
-        */
-       protected function initMagicQuotes() {
-               if (function_exists('get_magic_quotes_gpc')) {
-                       if (@get_magic_quotes_gpc()) {
-                               if (!empty($_REQUEST)) {
-                                       $_REQUEST = ArrayUtil::stripslashes($_REQUEST);
-                               }
-                               if (!empty($_POST)) {
-                                       $_POST = ArrayUtil::stripslashes($_POST);
-                               }
-                               if (!empty($_GET)) {
-                                       $_GET = ArrayUtil::stripslashes($_GET);
-                               }
-                               if (!empty($_COOKIE)) {
-                                       $_COOKIE = ArrayUtil::stripslashes($_COOKIE);
-                               }
-                               if (!empty($_FILES)) {
-                                       foreach ($_FILES as $name => $attributes) {
-                                               foreach ($attributes as $key => $value) {
-                                                       if ($key != 'tmp_name') {
-                                                               $_FILES[$name][$key] = ArrayUtil::stripslashes($value);
-                                                       }
-                                               }
-                                       }
-                               }
-                       }
-               }
-               
-               if (function_exists('set_magic_quotes_runtime')) {
-                       @set_magic_quotes_runtime(0);
-               }
-       }
-       
        /**
         * Returns the database object.
         * 
@@ -230,7 +228,7 @@ class WCF {
        /**
         * Returns the session object.
         * 
-        * @return      \wcf\system\session\SessionHandler
+        * @return      SessionHandler
         */
        public static final function getSession() {
                return self::$sessionObj;
@@ -257,7 +255,7 @@ class WCF {
        /**
         * Returns the template object.
         * 
-        * @return      \wcf\system\template\TemplateEngine
+        * @return      TemplateEngine
         */
        public static final function getTPL() {
                return self::$tplObj;
@@ -269,45 +267,76 @@ class WCF {
         * @param       \Exception      $e
         */
        public static final function handleException($e) {
-               try {
-                       if (!($e instanceof \Exception)) throw $e;
+               // backwards compatibility
+               if ($e instanceof IPrintableException) {
+                       $e->show();
+                       exit;
+               }
+               
+               if (ob_get_level()) {
+                       // discard any output generated before the exception occurred, prevents exception
+                       // being hidden inside HTML elements and therefore not visible in browser output
+                       // 
+                       // ob_get_level() can return values > 1, if the PHP setting `output_buffering` is on
+                       while (ob_get_level()) ob_end_clean();
                        
-                       if ($e instanceof IPrintableException) {
-                               $e->show();
-                               exit;
+                       // Some webservers are broken and will apply gzip encoding at all cost, but they fail
+                       // to set a proper `Content-Encoding` HTTP header and mess things up even more.
+                       // Especially the `identity` value appears to be unrecognized by some of them, hence
+                       // we'll just gzip the output of the exception to prevent them from tampering.
+                       // This part is copied from `HeaderUtil` in order to isolate the exception handler!
+                       if (defined('HTTP_ENABLE_GZIP') && HTTP_ENABLE_GZIP && !defined('HTTP_DISABLE_GZIP')) {
+                               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) {
+                                       if (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'x-gzip') !== false) {
+                                               @header('Content-Encoding: x-gzip');
+                                       }
+                                       else {
+                                               @header('Content-Encoding: gzip');
+                                       }
+                                       
+                                       ob_start(function($output) {
+                                               $size = strlen($output);
+                                               $crc = crc32($output);
+                                               
+                                               $newOutput = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff";
+                                               $newOutput .= substr(gzcompress($output, 1), 2, -4);
+                                               $newOutput .= pack('V', $crc);
+                                               $newOutput .= pack('V', $size);
+                                               
+                                               return $newOutput;
+                                       });
+                               }
                        }
-                       
-                       // repack Exception
-                       self::handleException(new SystemException($e->getMessage(), $e->getCode(), '', $e));
                }
-               catch (\Throwable $exception) {
-                       die("<pre>WCF::handleException() Unhandled exception: ".$exception->getMessage()."\n\n".preg_replace('/Database->__construct\(.*\)/', 'Database->__construct(...)', $exception->getTraceAsString()));
+               
+               @header('HTTP/1.1 503 Service Unavailable');
+               try {
+                       \wcf\functions\exception\printThrowable($e);
                }
-               catch (\Exception $exception) {
-                       die("<pre>WCF::handleException() Unhandled exception: ".$exception->getMessage()."\n\n".preg_replace('/Database->__construct\(.*\)/', 'Database->__construct(...)', $exception->getTraceAsString()));
+               catch (\Throwable $e2) {
+                       echo "<pre>An Exception was thrown while handling an Exception:\n\n";
+                       echo preg_replace('/Database->__construct\(.*\)/', 'Database->__construct(...)', $e2);
+                       echo "\n\nwas thrown while:\n\n";
+                       echo preg_replace('/Database->__construct\(.*\)/', 'Database->__construct(...)', $e);
+                       echo "\n\nwas handled.</pre>";
+                       exit;
                }
        }
        
        /**
-        * Catches php errors and throws instead a system exception.
+        * Turns PHP errors into an ErrorException.
         * 
-        * @param       integer         $errorNo
+        * @param       integer         $severity
         * @param       string          $message
-        * @param       string          $filename
-        * @param       integer         $lineNo
-        */
-       public static final function handleError($errorNo, $message, $filename, $lineNo) {
-               if (error_reporting() != 0) {
-                       $type = 'error';
-                       switch ($errorNo) {
-                               case 2: $type = 'warning';
-                                       break;
-                               case 8: $type = 'notice';
-                                       break;
-                       }
-                       
-                       throw new SystemException('PHP '.$type.' in file '.$filename.' ('.$lineNo.'): '.$message, 0);
-               }
+        * @param       string          $file
+        * @param       integer         $line
+        * @throws      ErrorException
+        */
+       public static final function handleError($severity, $message, $file, $line) {
+               // this is necessary for the shut-up operator
+               if (error_reporting() == 0) return;
+               
+               throw new ErrorException($message, 0, $severity, $file, $line);
        }
        
        /**
@@ -317,11 +346,10 @@ class WCF {
                // get configuration
                $dbHost = $dbUser = $dbPassword = $dbName = '';
                $dbPort = 0;
-               $dbClass = 'wcf\system\database\MySQLDatabase';
                require(WCF_DIR.'config.inc.php');
                
                // create database connection
-               self::$dbObj = new $dbClass($dbHost, $dbUser, $dbPassword, $dbName, $dbPort);
+               self::$dbObj = new MySQLDatabase($dbHost, $dbUser, $dbPassword, $dbName, $dbPort);
        }
        
        /**
@@ -335,6 +363,24 @@ class WCF {
                        OptionEditor::rebuild();
                }
                require_once($filename);
+               
+               // check if option file is complete and writable
+               if (PACKAGE_ID) {
+                       if (!is_writable($filename)) {
+                               FileUtil::makeWritable($filename);
+                               
+                               if (!is_writable($filename)) {
+                                       throw new SystemException("The option file '" . $filename . "' is not writable.");
+                               }
+                       }
+                       
+                       // check if a previous write operation was incomplete and force rebuilding
+                       if (!defined('WCF_OPTION_INC_PHP_SUCCESS')) {
+                               OptionEditor::rebuild();
+                               
+                               require_once($filename);
+                       }
+               }
        }
        
        /**
@@ -345,6 +391,7 @@ class WCF {
                $factory->load();
                
                self::$sessionObj = SessionHandler::getInstance();
+               self::$sessionObj->setHasValidCookie($factory->hasValidCookie());
        }
        
        /**
@@ -383,7 +430,8 @@ class WCF {
                        self::getSession()->setStyleID(intval($_REQUEST['styleID']));
                }
                
-               StyleHandler::getInstance()->changeStyle(self::getSession()->getStyleID());
+               $styleHandler = StyleHandler::getInstance();
+               $styleHandler->changeStyle(self::getSession()->getStyleID());
        }
        
        /**
@@ -395,7 +443,7 @@ class WCF {
                if (defined('BLACKLIST_IP_ADDRESSES') && BLACKLIST_IP_ADDRESSES != '') {
                        if (!StringUtil::executeWordFilter(UserUtil::convertIPv6To4(self::getSession()->ipAddress), BLACKLIST_IP_ADDRESSES)) {
                                if ($isAjax) {
-                                       throw new AJAXException(self::getLanguage()->get('wcf.ajax.error.permissionDenied'), AJAXException::INSUFFICIENT_PERMISSIONS);
+                                       throw new AJAXException(self::getLanguage()->getDynamicVariable('wcf.ajax.error.permissionDenied'), AJAXException::INSUFFICIENT_PERMISSIONS);
                                }
                                else {
                                        throw new PermissionDeniedException();
@@ -403,7 +451,7 @@ class WCF {
                        }
                        else if (!StringUtil::executeWordFilter(self::getSession()->ipAddress, BLACKLIST_IP_ADDRESSES)) {
                                if ($isAjax) {
-                                       throw new AJAXException(self::getLanguage()->get('wcf.ajax.error.permissionDenied'), AJAXException::INSUFFICIENT_PERMISSIONS);
+                                       throw new AJAXException(self::getLanguage()->getDynamicVariable('wcf.ajax.error.permissionDenied'), AJAXException::INSUFFICIENT_PERMISSIONS);
                                }
                                else {
                                        throw new PermissionDeniedException();
@@ -413,7 +461,7 @@ class WCF {
                if (defined('BLACKLIST_USER_AGENTS') && BLACKLIST_USER_AGENTS != '') {
                        if (!StringUtil::executeWordFilter(self::getSession()->userAgent, BLACKLIST_USER_AGENTS)) {
                                if ($isAjax) {
-                                       throw new AJAXException(self::getLanguage()->get('wcf.ajax.error.permissionDenied'), AJAXException::INSUFFICIENT_PERMISSIONS);
+                                       throw new AJAXException(self::getLanguage()->getDynamicVariable('wcf.ajax.error.permissionDenied'), AJAXException::INSUFFICIENT_PERMISSIONS);
                                }
                                else {
                                        throw new PermissionDeniedException();
@@ -423,7 +471,7 @@ class WCF {
                if (defined('BLACKLIST_HOSTNAMES') && BLACKLIST_HOSTNAMES != '') {
                        if (!StringUtil::executeWordFilter(@gethostbyaddr(self::getSession()->ipAddress), BLACKLIST_HOSTNAMES)) {
                                if ($isAjax) {
-                                       throw new AJAXException(self::getLanguage()->get('wcf.ajax.error.permissionDenied'), AJAXException::INSUFFICIENT_PERMISSIONS);
+                                       throw new AJAXException(self::getLanguage()->getDynamicVariable('wcf.ajax.error.permissionDenied'), AJAXException::INSUFFICIENT_PERMISSIONS);
                                }
                                else {
                                        throw new PermissionDeniedException();
@@ -437,6 +485,16 @@ class WCF {
                                throw new AJAXException(self::getLanguage()->getDynamicVariable('wcf.user.error.isBanned'), AJAXException::INSUFFICIENT_PERMISSIONS);
                        }
                        else {
+                               self::$forceLogout = true;
+                               
+                               // remove cookies
+                               if (isset($_COOKIE[COOKIE_PREFIX.'userID'])) {
+                                       HeaderUtil::setCookie('userID', 0);
+                               }
+                               if (isset($_COOKIE[COOKIE_PREFIX.'password'])) {
+                                       HeaderUtil::setCookie('password', '');
+                               }
+                               
                                throw new NamedUserException(self::getLanguage()->getDynamicVariable('wcf.user.error.isBanned'));
                        }
                }
@@ -447,49 +505,61 @@ class WCF {
         */
        protected function initApplications() {
                // step 1) load all applications
-               $loadedApplications = array();
+               $loadedApplications = [];
                
                // register WCF as application
-               self::$applications['wcf'] = ApplicationHandler::getInstance()->getWCF();
+               self::$applications['wcf'] = ApplicationHandler::getInstance()->getApplicationByID(1);
                
-               if (PACKAGE_ID == 1) {
-                       return;
+               if (!class_exists(WCFACP::class, false)) {
+                       static::getTPL()->assign('baseHref', self::$applications['wcf']->getPageURL());
                }
                
                // start main application
                $application = ApplicationHandler::getInstance()->getActiveApplication();
-               $loadedApplications[] = $this->loadApplication($application);
-               
-               // register primary application
-               $abbreviation = ApplicationHandler::getInstance()->getAbbreviation($application->packageID);
-               self::$applications[$abbreviation] = $application;
+               if ($application->packageID != 1) {
+                       $loadedApplications[] = $this->loadApplication($application);
+                       
+                       // register primary application
+                       $abbreviation = ApplicationHandler::getInstance()->getAbbreviation($application->packageID);
+                       self::$applications[$abbreviation] = $application;
+               }
                
                // start dependent applications
                $applications = ApplicationHandler::getInstance()->getDependentApplications();
                foreach ($applications as $application) {
+                       if ($application->packageID == 1) {
+                               // ignore WCF
+                               continue;
+                       }
+                       else if ($application->isTainted) {
+                               // ignore apps flagged for uninstallation
+                               continue;
+                       }
+                       
                        $loadedApplications[] = $this->loadApplication($application, true);
                }
                
                // step 2) run each application
                if (!class_exists('wcf\system\WCFACP', false)) {
+                       /** @var IApplication $application */
                        foreach ($loadedApplications as $application) {
                                $application->__run();
                        }
                        
                        // refresh the session 1 minute before it expires
-                       self::getTPL()->assign('__sessionKeepAlive', (SESSION_TIMEOUT - 60));
+                       self::getTPL()->assign('__sessionKeepAlive', SESSION_TIMEOUT - 60);
                }
        }
        
        /**
         * Loads an application.
         * 
-        * @param       \wcf\data\application\Application               $application
-        * @param       boolean                                         $isDependentApplication
-        * @return      \wcf\system\application\IApplication
+        * @param       Application             $application
+        * @param       boolean                 $isDependentApplication
+        * @return      IApplication
+        * @throws      SystemException
         */
        protected function loadApplication(Application $application, $isDependentApplication = false) {
-               $applicationObject = null;
                $package = PackageCache::getInstance()->getPackage($application->packageID);
                // package cache might be outdated
                if ($package === null) {
@@ -504,15 +574,39 @@ class WCF {
                                throw new SystemException("application identified by package id '".$application->packageID."' is unknown");
                        }
                }
-                       
+               
                $abbreviation = ApplicationHandler::getInstance()->getAbbreviation($application->packageID);
                $packageDir = FileUtil::getRealPath(WCF_DIR.$package->packageDir);
                self::$autoloadDirectories[$abbreviation] = $packageDir . 'lib/';
                
                $className = $abbreviation.'\system\\'.strtoupper($abbreviation).'Core';
-               if (class_exists($className) && ClassUtil::isInstanceOf($className, 'wcf\system\application\IApplication')) {
+               
+               // class was not found, possibly the app was moved, but `packageDir` has not been adjusted
+               if (!class_exists($className)) {
+                       // check if both the Core and the app are on the same domain
+                       $coreApp = ApplicationHandler::getInstance()->getApplicationByID(1);
+                       if ($coreApp->domainName === $application->domainName) {
+                               // resolve the relative path and use it to construct the autoload directory
+                               $relativePath = FileUtil::getRelativePath($coreApp->domainPath, $application->domainPath);
+                               if ($relativePath !== './') {
+                                       $packageDir = FileUtil::getRealPath(WCF_DIR.$relativePath);
+                                       self::$autoloadDirectories[$abbreviation] = $packageDir . 'lib/';
+                                       
+                                       if (class_exists($className)) {
+                                               // the class can now be found, update the `packageDir` value
+                                               (new PackageEditor($package))->update(['packageDir' => $relativePath]);
+                                       }
+                               }
+                       }
+               }
+               
+               if (class_exists($className) && is_subclass_of($className, IApplication::class)) {
                        // include config file
                        $configPath = $packageDir . PackageInstallationDispatcher::CONFIG_FILE;
+                       if (!file_exists($configPath)) {
+                               Package::writeConfigFile($package->packageID);
+                       }
+                       
                        if (file_exists($configPath)) {
                                require_once($configPath);
                        }
@@ -523,25 +617,27 @@ class WCF {
                        // register template path if not within ACP
                        if (!class_exists('wcf\system\WCFACP', false)) {
                                // add template path and abbreviation
-                               $this->getTPL()->addApplication($abbreviation, $packageDir . 'templates/');
+                               static::getTPL()->addApplication($abbreviation, $packageDir . 'templates/');
                        }
+                       EmailTemplateEngine::getInstance()->addApplication($abbreviation, $packageDir . 'templates/');
                        
                        // init application and assign it as template variable
-                       self::$applicationObjects[$application->packageID] = call_user_func(array($className, 'getInstance'));
-                       $this->getTPL()->assign('__'.$abbreviation, self::$applicationObjects[$application->packageID]);
+                       self::$applicationObjects[$application->packageID] = call_user_func([$className, 'getInstance']);
+                       static::getTPL()->assign('__'.$abbreviation, self::$applicationObjects[$application->packageID]);
+                       EmailTemplateEngine::getInstance()->assign('__'.$abbreviation, self::$applicationObjects[$application->packageID]);
                }
                else {
                        unset(self::$autoloadDirectories[$abbreviation]);
-                       throw new SystemException("Unable to run '".$package->package."', '".$className."' is missing or does not implement 'wcf\system\application\IApplication'.");
+                       throw new SystemException("Unable to run '".$package->package."', '".$className."' is missing or does not implement '".IApplication::class."'.");
                }
                
                // register template path in ACP
                if (class_exists('wcf\system\WCFACP', false)) {
-                       $this->getTPL()->addApplication($abbreviation, $packageDir . 'acp/templates/');
+                       static::getTPL()->addApplication($abbreviation, $packageDir . 'acp/templates/');
                }
                else if (!$isDependentApplication) {
                        // assign base tag
-                       $this->getTPL()->assign('baseHref', $application->getPageURL());
+                       static::getTPL()->assign('baseHref', $application->getPageURL());
                }
                
                // register application
@@ -553,8 +649,8 @@ class WCF {
        /**
         * Returns the corresponding application object. Does not support the 'wcf' pseudo application.
         * 
-        * @param       \wcf\data\application\Application       $application
-        * @return      \wcf\system\application\IApplication
+        * @param       Application     $application
+        * @return      IApplication
         */
        public static function getApplicationObject(Application $application) {
                if (isset(self::$applicationObjects[$application->packageID])) {
@@ -564,6 +660,16 @@ class WCF {
                return null;
        }
        
+       /**
+        * Returns the invoked application.
+        * 
+        * @return      Application
+        * @since       3.1
+        */
+       public static function getActiveApplication() {
+               return ApplicationHandler::getInstance()->getActiveApplication();
+       }
+       
        /**
         * Loads an application on runtime, do not use this outside the package installation.
         * 
@@ -596,11 +702,28 @@ class WCF {
         * Assigns some default variables to the template engine.
         */
        protected function assignDefaultTemplateVariables() {
-               self::getTPL()->registerPrefilter(array('event', 'hascontent', 'lang'));
-               self::getTPL()->assign(array(
+               self::getTPL()->registerPrefilter(['event', 'hascontent', 'lang']);
+               self::getTPL()->assign([
                        '__wcf' => $this,
-                       '__wcfVersion' => LAST_UPDATE_TIME // @deprecated since 2.1, use LAST_UPDATE_TIME directly
-               ));
+                       '__wcfVersion' => LAST_UPDATE_TIME // @deprecated 2.1, use LAST_UPDATE_TIME directly
+               ]);
+               
+               $isAjax = isset($_SERVER['HTTP_X_REQUESTED_WITH']) && ($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest');
+               // Execute background queue in this request, if it was requested and AJAX isn't used.
+               if (!$isAjax) {
+                       if (self::getSession()->getVar('forceBackgroundQueuePerform')) {
+                               self::getTPL()->assign([
+                                       'forceBackgroundQueuePerform' => true
+                               ]);
+                               
+                               self::getSession()->unregister('forceBackgroundQueuePerform');
+                       }
+               }
+               
+               EmailTemplateEngine::getInstance()->registerPrefilter(['event', 'hascontent', 'lang']);
+               EmailTemplateEngine::getInstance()->assign([
+                       '__wcf' => $this
+               ]);
        }
        
        /**
@@ -608,6 +731,7 @@ class WCF {
         * 
         * @param       string          $name
         * @return      mixed           value
+        * @throws      SystemException
         */
        public function __get($name) {
                $method = 'get'.ucfirst($name);
@@ -618,14 +742,33 @@ class WCF {
                throw new SystemException("method '".$method."' does not exist in class WCF");
        }
        
+       /**
+        * Returns true if current application (WCF) is treated as active and was invoked directly.
+        *
+        * @return      boolean
+        */
+       public function isActiveApplication() {
+               return (ApplicationHandler::getInstance()->getActiveApplication()->packageID == 1);
+       }
+       
        /**
         * Changes the active language.
         * 
         * @param       integer         $languageID
         */
        public static final function setLanguage($languageID) {
+               if (!$languageID || LanguageFactory::getInstance()->getLanguage($languageID) === null) {
+                       $languageID = LanguageFactory::getInstance()->getDefaultLanguageID();
+               }
+               
                self::$languageObj = LanguageFactory::getInstance()->getLanguage($languageID);
-               self::getTPL()->setLanguageID(self::getLanguage()->languageID);
+               
+               // the template engine may not be available yet, usually happens when
+               // changing the user (and thus the language id) during session init
+               if (self::$tplObj !== null) {
+                       self::getTPL()->setLanguageID(self::getLanguage()->languageID);
+                       EmailTemplateEngine::getInstance()->setLanguageID(self::getLanguage()->languageID);
+               }
        }
        
        /**
@@ -651,7 +794,7 @@ class WCF {
        }
        
        /**
-        * @see \wcf\system\WCF::__callStatic()
+        * @inheritDoc
         */
        public final function __call($name, array $arguments) {
                // bug fix to avoid php crash, see http://bugs.php.net/bug.php?id=55020
@@ -667,6 +810,8 @@ class WCF {
         * 
         * @param       string          $name
         * @param       array           $arguments
+        * @return      object
+        * @throws      SystemException
         */
        public static final function __callStatic($name, array $arguments) {
                $className = preg_replace('~^get~', '', $name);
@@ -681,11 +826,11 @@ class WCF {
                }
                
                if (class_exists($objectName)) {
-                       if (!(ClassUtil::isInstanceOf($objectName, 'wcf\system\SingletonFactory'))) {
-                               throw new SystemException("class '".$objectName."' does not implement the interface 'SingletonFactory'");
+                       if (!is_subclass_of($objectName, SingletonFactory::class)) {
+                               throw new ParentClassException($objectName, SingletonFactory::class);
                        }
                        
-                       self::$coreObject[$className] = call_user_func(array($objectName, 'getInstance'));
+                       self::$coreObject[$className] = call_user_func([$objectName, 'getInstance']);
                        return self::$coreObject[$className];
                }
        }
@@ -752,6 +897,20 @@ class WCF {
                return self::$applications[$abbreviation]->getPageURL();
        }
        
+       /**
+        * Returns the domain path for the currently active application,
+        * used to avoid CORS requests.
+        * 
+        * @return      string
+        */
+       public static function getActivePath() {
+               if (!PACKAGE_ID) {
+                       return self::getPath();
+               }
+               
+               return self::getPath(ApplicationHandler::getInstance()->getAbbreviation(ApplicationHandler::getInstance()->getActiveApplication()->packageID));
+       }
+       
        /**
         * Returns a fully qualified anchor for current page.
         * 
@@ -763,39 +922,43 @@ class WCF {
        }
        
        /**
-        * Returns the URI of the current page.
+        * Returns the currently active page or null if unknown.
         * 
-        * @return      string
+        * @return      Page|null
         */
-       public static function getRequestURI() {
-               if (URL_LEGACY_MODE) {
-                       // resolve path and query components
-                       $scriptName = $_SERVER['SCRIPT_NAME'];
-                       $pathInfo = RouteHandler::getPathInfo();
-                       if (empty($pathInfo)) {
-                               // bug fix if URL omits script name and path
-                               $scriptName = substr($scriptName, 0, strrpos($scriptName, '/'));
-                       }
-                       
-                       $path = str_replace('/index.php', '', str_replace($scriptName, '', $_SERVER['REQUEST_URI']));
-                       if (!StringUtil::isUTF8($path)) {
-                               $path = StringUtil::convertEncoding('ISO-8859-1', 'UTF-8', $path);
-                       }
-                       $path = FileUtil::removeLeadingSlash($path);
-                       $baseHref = self::getTPL()->get('baseHref');
-                       
-                       if (!empty($path) && mb_strpos($path, '?') !== 0) {
-                               $baseHref .= 'index.php/';
-                       }
-                       
-                       return $baseHref . $path;
+       public static function getActivePage() {
+               if (self::getActiveRequest() === null) {
+                       return null;
                }
-               else {
-                       $url = preg_replace('~^(https?://[^/]+)(?:/.*)?$~', '$1', self::getTPL()->get('baseHref'));
-                       $url .= $_SERVER['REQUEST_URI'];
+               
+               if (self::getActiveRequest()->getClassName() === CmsPage::class) {
+                       $metaData = self::getActiveRequest()->getMetaData();
+                       if (isset($metaData['cms'])) {
+                               return PageCache::getInstance()->getPage($metaData['cms']['pageID']);
+                       }
                        
-                       return $url;
+                       return null;
                }
+               
+               return PageCache::getInstance()->getPageByController(self::getActiveRequest()->getClassName());
+       }
+       
+       /**
+        * Returns the currently active request.
+        * 
+        * @return Request
+        */
+       public static function getActiveRequest() {
+               return RequestHandler::getInstance()->getActiveRequest();
+       }
+       
+       /**
+        * Returns the URI of the current page.
+        * 
+        * @return      string
+        */
+       public static function getRequestURI() {
+               return preg_replace('~^(https?://[^/]+)(?:/.*)?$~', '$1', self::getTPL()->get('baseHref')) . $_SERVER['REQUEST_URI'];
        }
        
        /**
@@ -826,12 +989,22 @@ class WCF {
        /**
         * Returns style handler.
         * 
-        * @return      \wcf\system\style\StyleHandler
+        * @return      StyleHandler
         */
        public function getStyleHandler() {
                return StyleHandler::getInstance();
        }
        
+       /**
+        * Returns box handler.
+        *
+        * @return      BoxHandler
+        * @since       3.0
+        */
+       public function getBoxHandler() {
+               return BoxHandler::getInstance();
+       }
+       
        /**
         * Returns number of available updates.
         * 
@@ -859,16 +1032,73 @@ class WCF {
        public function getFavicon() {
                $activeApplication = ApplicationHandler::getInstance()->getActiveApplication();
                $wcf = ApplicationHandler::getInstance()->getWCF();
+               $favicon = StyleHandler::getInstance()->getStyle()->getRelativeFavicon();
                
                if ($activeApplication->domainName !== $wcf->domainName) {
-                       if (file_exists(WCF_DIR.'images/favicon.ico')) {
-                               $favicon = file_get_contents(WCF_DIR.'images/favicon.ico');
+                       if (file_exists(WCF_DIR.$favicon)) {
+                               $favicon = file_get_contents(WCF_DIR.$favicon);
                                
                                return 'data:image/x-icon;base64,' . base64_encode($favicon);
                        }
                }
                
-               return self::getPath() . 'images/favicon.ico';
+               return self::getPath() . $favicon;
+       }
+       
+       /**
+        * Returns true if the desktop notifications should be enabled.
+        * 
+        * @return      boolean
+        */
+       public function useDesktopNotifications() {
+               if (!ENABLE_DESKTOP_NOTIFICATIONS) {
+                       return false;
+               }
+               else if (ApplicationHandler::getInstance()->isMultiDomainSetup()) {
+                       $application = ApplicationHandler::getInstance()->getApplicationByID(DESKTOP_NOTIFICATION_PACKAGE_ID);
+                       // mismatch, default to Core
+                       if ($application === null) $application = ApplicationHandler::getInstance()->getApplicationByID(1);
+                       
+                       $currentApplication = ApplicationHandler::getInstance()->getActiveApplication();
+                       if ($currentApplication->domainName != $application->domainName) {
+                               // different domain
+                               return false;
+                       }
+               }
+               
+               return true;
+       }
+       
+       /**
+        * Returns true if currently active request represents the landing page.
+        * 
+        * @return      boolean
+        */
+       public static function isLandingPage() {
+               if (self::getActiveRequest() === null) {
+                       return false;
+               }
+               
+               return self::getActiveRequest()->isLandingPage();
+       }
+       
+       /**
+        * Returns true if the given API version is currently supported.
+        * 
+        * @param       integer         $apiVersion
+        * @return      boolean
+        */
+       public static function isSupportedApiVersion($apiVersion) {
+               return ($apiVersion == WSC_API_VERSION) || in_array($apiVersion, self::$supportedLegacyApiVersions);
+       }
+       
+       /**
+        * Returns the list of supported legacy API versions.
+        * 
+        * @return      integer[]
+        */
+       public static function getSupportedLegacyApiVersions() {
+               return self::$supportedLegacyApiVersions;
        }
        
        /**
@@ -876,7 +1106,7 @@ class WCF {
         */
        protected function initCronjobs() {
                if (PACKAGE_ID) {
-                       self::getTPL()->assign('executeCronjobs', (CronjobScheduler::getInstance()->getNextExec() < TIME_NOW && defined('OFFLINE') && !OFFLINE));
+                       self::getTPL()->assign('executeCronjobs', CronjobScheduler::getInstance()->getNextExec() < TIME_NOW && defined('OFFLINE') && !OFFLINE);
                }
        }
 }