Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions wcfsetup/install/files/global.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion wcfsetup/install/files/lib/data/package/Package.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -447,9 +447,9 @@ public static function writeConfigFile($packageID)
$content = "<?php\n";
$content .= "// {$package->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";
Expand Down
79 changes: 78 additions & 1 deletion wcfsetup/install/files/lib/system/WCF.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -194,6 +195,7 @@ public function __construct()
// start initialization
$this->initDB();
$this->loadOptions();
$this->resolveActiveApplication();
$this->initSession();
$this->initLanguage();
$this->initTPL();
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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.
*
Expand Down
1 change: 1 addition & 0 deletions wcfsetup/install/files/lib/system/WCFACP.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public function __construct()
// start initialization
$this->initDB();
$this->loadOptions();
$this->resolveActiveApplication();
$this->initSession();
$this->initLanguage();
$this->initTPL();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,20 @@
final class ApplicationHandler extends SingletonFactory
{
/**
* application cache
* @var mixed[][]
* @var array{
* abbreviation: array<string, int>,
* application: array<int, Application>,
* sortedPaths: array<int, string>,
* rootApplication: ?int,
* }
*/
protected $cache;
private array $cache;

/**
* list of page URLs
* @var string[]
*/
protected array $pageURLs = [];
private array $pageURLs;

/**
* Initializes cache.
Expand Down Expand Up @@ -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([
Expand Down Expand Up @@ -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
Expand All @@ -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<int, string>
* @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.
*/
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@
/**
* Caches applications.
*
* @author Alexander Ebert
* @copyright 2001-2019 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @author Alexander Ebert
* @copyright 2001-2025 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
*/
class ApplicationCacheBuilder extends AbstractCacheBuilder
final class ApplicationCacheBuilder extends AbstractCacheBuilder
{
/**
* @inheritDoc
*/
#[\Override]
public function rebuild(array $parameters)
{
$data = [
'abbreviation' => [],
'application' => [],
'sortedPaths' => [],
'rootApplication' => null,
];

// fetch applications
Expand All @@ -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
Expand All @@ -49,4 +53,22 @@ public function rebuild(array $parameters)

return $data;
}

/**
* @param array<int, string> $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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
13 changes: 13 additions & 0 deletions wcfsetup/install/files/lib/system/request/RouteHandler.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}