diff --git a/wcfsetup/install/files/global.php b/wcfsetup/install/files/global.php index 4dcb1a5ef9e..a4e7e512261 100644 --- a/wcfsetup/install/files/global.php +++ b/wcfsetup/install/files/global.php @@ -10,11 +10,16 @@ require_once(__DIR__ . '/app.config.inc.php'); // Make the frontend inaccessible until WCFSetup completes. +/* + +TODO: This is currently not possible, find a solution! + if (!PACKAGE_ID) { \http_response_code(500); exit; } +*/ // initiate wcf core require_once(WCF_DIR . 'lib/system/WCF.class.php'); diff --git a/wcfsetup/install/files/lib/data/package/Package.class.php b/wcfsetup/install/files/lib/data/package/Package.class.php index 7b41028ccde..b2ae5398556 100644 --- a/wcfsetup/install/files/lib/data/package/Package.class.php +++ b/wcfsetup/install/files/lib/data/package/Package.class.php @@ -447,9 +447,9 @@ public static function writeConfigFile($packageID) $content = "package} (packageID {$packageID})\n"; $content .= "if (!defined('{$prefix}_DIR')) define('{$prefix}_DIR', __DIR__.'/');\n"; - $content .= "if (!defined('PACKAGE_ID')) define('PACKAGE_ID', {$packageID});\n"; if ($packageID != 1) { + $content .= "if (!defined('PACKAGE_ID')) define('PACKAGE_ID', {$packageID});\n"; $content .= "\n"; $content .= "// helper constants for applications\n"; $content .= "if (!defined('RELATIVE_{$prefix}_DIR')) define('RELATIVE_{$prefix}_DIR', '');\n"; diff --git a/wcfsetup/install/files/lib/system/WCF.class.php b/wcfsetup/install/files/lib/system/WCF.class.php index 8842b33f38b..5b417abe32b 100644 --- a/wcfsetup/install/files/lib/system/WCF.class.php +++ b/wcfsetup/install/files/lib/system/WCF.class.php @@ -24,6 +24,7 @@ use wcf\system\registry\RegistryHandler; use wcf\system\request\Request; use wcf\system\request\RequestHandler; +use wcf\system\request\RouteHandler; use wcf\system\session\SessionFactory; use wcf\system\session\SessionHandler; use wcf\system\style\StyleHandler; @@ -194,6 +195,7 @@ public function __construct() // start initialization $this->initDB(); $this->loadOptions(); + $this->resolveActiveApplication(); $this->initSession(); $this->initLanguage(); $this->initTPL(); @@ -418,7 +420,7 @@ protected function loadOptions(): void require($filename); // check if option file is complete and writable - if (PACKAGE_ID) { + if (!\defined('\\PACKAGE_ID')) { if (!\is_writable($filename)) { FileUtil::makeWritable($filename); @@ -450,6 +452,81 @@ protected function loadOptions(): void } } + protected function resolveActiveApplication(): void + { + if (\defined('PACKAGE_ID')) { + return; + } + + $applications = ApplicationHandler::getInstance()->getApplications(); + if (!\URL_OMIT_INDEX_PHP || \count($applications) === 1) { + \define('PACKAGE_ID', 1); + return; + } + + // We do not support smart rewrites for setups where apps are installed + // in different directory where the only shared ancestor is not an app. + $rootApp = ApplicationHandler::getInstance()->getRootApplication(); + if ($rootApp === null) { + \define('PACKAGE_ID', 1); + return; + } + + $sortedPaths = ApplicationHandler::getInstance()->getSortedPaths(); + + // When the core is the root app we can simply check the path info for + // any apps appearing at the start of it. + $coreIsAtRoot = ($rootApp === ApplicationHandler::getInstance()->getWCF()); + + /** @var ?int */ + $candidate = null; + $pathInfo = RouteHandler::getPathInfo(); + if ($coreIsAtRoot) { + foreach ($sortedPaths as $packageID => $pathname) { + if (\str_starts_with($pathInfo, \mb_substr($pathname, \mb_strlen($rootApp->domainPath)))) { + $candidate = $packageID; + break; + } + } + + \assert($candidate !== null); + + $app = ApplicationHandler::getInstance()->getApplicationByID($candidate); + $prefix = \mb_substr($app->domainPath, \mb_strlen($rootApp->domainPath)); + RouteHandler::ltrimPathInfo($prefix); + } else { + // The path info is relative to the root app, therefore we need to + // resolve the invoked app by examining the start of it. + $lengthOfRootPath = \mb_strlen($rootApp->domainPath); + foreach ($sortedPaths as $packageID => $pathname) { + $pathname = \mb_substr($pathname, $lengthOfRootPath); + if (\str_starts_with($pathInfo, $pathname)) { + $candidate = $packageID; + + // Trim the path info to strip the invoekd app from it. + RouteHandler::ltrimPathInfo($pathname); + + break; + } + } + + \assert($candidate !== null); + } + + if ($candidate === null) { + \define('PACKAGE_ID', 1); + } else { + $application = ApplicationHandler::getInstance()->getApplicationByID($candidate); + \assert($application !== null); + + \define('PACKAGE_ID', $candidate); + + // Include the `app.config.inc.php` of the primary app. + $pathname = FileUtil::addTrailingSlash(FileUtil::getRealPath(\WCF_DIR . $application->getPackage()->packageDir)) . 'app.config.inc.php'; + require_once $pathname; + } + } + /** * Defines constants for obsolete options, which were removed. * diff --git a/wcfsetup/install/files/lib/system/WCFACP.class.php b/wcfsetup/install/files/lib/system/WCFACP.class.php index acd7e9aaea1..a77ff33f294 100644 --- a/wcfsetup/install/files/lib/system/WCFACP.class.php +++ b/wcfsetup/install/files/lib/system/WCFACP.class.php @@ -47,6 +47,7 @@ public function __construct() // start initialization $this->initDB(); $this->loadOptions(); + $this->resolveActiveApplication(); $this->initSession(); $this->initLanguage(); $this->initTPL(); diff --git a/wcfsetup/install/files/lib/system/application/ApplicationHandler.class.php b/wcfsetup/install/files/lib/system/application/ApplicationHandler.class.php index 8a599987bde..1ad522ab30b 100644 --- a/wcfsetup/install/files/lib/system/application/ApplicationHandler.class.php +++ b/wcfsetup/install/files/lib/system/application/ApplicationHandler.class.php @@ -26,16 +26,20 @@ final class ApplicationHandler extends SingletonFactory { /** - * application cache - * @var mixed[][] + * @var array{ + * abbreviation: array, + * application: array, + * sortedPaths: array, + * rootApplication: ?int, + * } */ - protected $cache; + private array $cache; /** * list of page URLs * @var string[] */ - protected array $pageURLs = []; + private array $pageURLs; /** * Initializes cache. @@ -185,7 +189,7 @@ public function getAbbreviations(): array */ public function isInternalURL(string $url): bool { - if (empty($this->pageURLs)) { + if (!isset($this->pageURLs)) { $internalHostnames = ArrayUtil::trim(\explode("\n", StringUtil::unifyNewlines(\INTERNAL_HOSTNAMES))); $this->pageURLs = \array_unique([ @@ -219,9 +223,7 @@ public function isMultiDomainSetup(): bool * @since 5.2 * @deprecated 5.5 - This function is a noop. The 'active' status is determined live. */ - public function rebuildActiveApplication(): void - { - } + public function rebuildActiveApplication(): void {} /** * @since 6.0 @@ -231,6 +233,37 @@ public function getDomainName(): string return $this->getApplicationByID(1)->domainName; } + /** + * Returns a list of the domain paths of all apps sorted by their length + * with the longest value appearing first. The key of each path is the + * package id of the corresponding app. + * + * @return array + * @since 6.2 + */ + public function getSortedPaths(): array + { + return $this->cache['sortedPaths']; + } + + /** + * Returns the app that is the root of all other apps. This is the case when + * all other apps installed in a direct or indirect subdirectory. + * + * @since 6.2 + */ + public function getRootApplication(): ?Application + { + if ($this->cache['rootApplication'] === null) { + return null; + } + + $rootApp = $this->getApplicationByID($this->cache['rootApplication']); + \assert($rootApp !== null); + + return $rootApp; + } + /** * Rebuilds cookie domain/path for all applications. */ @@ -263,7 +296,7 @@ public static function insertRealDatabaseTableNames(string $string, bool $skipCa } if ($skipCache) { - $sql = "SELECT package + $sql = "SELECT package FROM wcf" . WCF_N . "_package WHERE isApplication = ?"; $statement = WCF::getDB()->prepareUnmanaged($sql); diff --git a/wcfsetup/install/files/lib/system/cache/builder/ApplicationCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/ApplicationCacheBuilder.class.php index 1333b423db0..1c3e9dcb936 100644 --- a/wcfsetup/install/files/lib/system/cache/builder/ApplicationCacheBuilder.class.php +++ b/wcfsetup/install/files/lib/system/cache/builder/ApplicationCacheBuilder.class.php @@ -9,20 +9,20 @@ /** * Caches applications. * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License + * @author Alexander Ebert + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License */ -class ApplicationCacheBuilder extends AbstractCacheBuilder +final class ApplicationCacheBuilder extends AbstractCacheBuilder { - /** - * @inheritDoc - */ + #[\Override] public function rebuild(array $parameters) { $data = [ 'abbreviation' => [], 'application' => [], + 'sortedPaths' => [], + 'rootApplication' => null, ]; // fetch applications @@ -34,8 +34,12 @@ public function rebuild(array $parameters) foreach ($applications as $application) { $data['application'][$application->packageID] = $application; + $data['sortedPaths'][$application->packageID] = $application->domainPath; } + \uasort($data['sortedPaths'], static fn($a, $b) => \mb_strlen($b) - \mb_strlen($a)); + $data['rootApplication'] = $this->getRootApplication($data['sortedPaths']); + // fetch abbreviations $sql = "SELECT packageID, package FROM wcf" . WCF_N . "_package @@ -49,4 +53,22 @@ public function rebuild(array $parameters) return $data; } + + /** + * @param array $sortedPaths + * @since 6.2 + */ + private function getRootApplication(array $sortedPaths): ?int + { + $candidate = \array_key_last($sortedPaths); + $shortestPath = $sortedPaths[$candidate]; + + foreach ($sortedPaths as $path) { + if (!\str_starts_with($path, $shortestPath)) { + return null; + } + } + + return $candidate; + } } diff --git a/wcfsetup/install/files/lib/system/request/RequestHandler.class.php b/wcfsetup/install/files/lib/system/request/RequestHandler.class.php index 49fe71265f0..a2e501843b4 100644 --- a/wcfsetup/install/files/lib/system/request/RequestHandler.class.php +++ b/wcfsetup/install/files/lib/system/request/RequestHandler.class.php @@ -89,6 +89,15 @@ protected function init() */ public function handle(string $application = 'wcf', bool $isACPRequest = false): void { + // Override the application when this request was the result of a smart + // rewrite. + if ($application === 'wcf' && \PACKAGE_ID > 1) { + $app = ApplicationHandler::getInstance()->getApplicationByID(\PACKAGE_ID); + \assert($app !== null); + + $application = $app->getAbbreviation(); + } + try { $this->isACPRequest = $isACPRequest; diff --git a/wcfsetup/install/files/lib/system/request/RouteHandler.class.php b/wcfsetup/install/files/lib/system/request/RouteHandler.class.php index 26daced5e1b..a4f779292a5 100644 --- a/wcfsetup/install/files/lib/system/request/RouteHandler.class.php +++ b/wcfsetup/install/files/lib/system/request/RouteHandler.class.php @@ -384,4 +384,17 @@ public static function getPathInfo(): string return self::$pathInfo; } + + /** + * TODO: This is merely a helper to get this working for the time being. + * + * @since 6.2 + */ + public static function ltrimPathInfo(string $prefix): void + { + \assert(isset(self::$pathInfo)); + \assert(\str_starts_with(self::$pathInfo, $prefix)); + + self::$pathInfo = \mb_substr(self::$pathInfo, \mb_strlen($prefix)); + } }