Skip to content
265 changes: 168 additions & 97 deletions lib/base.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
use OCP\Util;
use Psr\Log\LoggerInterface;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use function OCP\Log\logger;

require_once 'public/Constants.php';
Expand Down Expand Up @@ -237,28 +238,21 @@ public static function checkInstalled(\OC\SystemConfig $systemConfig): void {
}
}

public static function checkMaintenanceMode(\OC\SystemConfig $systemConfig): void {
// Allow ajax update script to execute without being stopped
if (((bool)$systemConfig->getValue('maintenance', false)) && OC::$SUBURI !== '/core/ajax/update.php') {
// send http status 503
http_response_code(503);
header('X-Nextcloud-Maintenance-Mode: 1');
header('Retry-After: 120');

// render error page
$template = Server::get(ITemplateManager::class)->getTemplate('', 'update.user', 'guest');
\OCP\Util::addScript('core', 'maintenance');
\OCP\Util::addScript('core', 'common');
\OCP\Util::addStyle('core', 'guest');
$template->printPage();
die();
}
public static function renderMaintenancePage(\OC\SystemConfig $systemConfig): void {
http_response_code(503);
header('X-Nextcloud-Maintenance-Mode: 1');
header('Retry-After: 120');
$template = Server::get(ITemplateManager::class)->getTemplate('', 'update.user', 'guest');
Util::addScript('core', 'maintenance');
Util::addScript('core', 'common');
Util::addStyle('core', 'guest');
$template->printPage();
}

/**
* Prints the upgrade page
*/
private static function printUpgradePage(\OC\SystemConfig $systemConfig): void {
private static function renderUpgradePage(\OC\SystemConfig $systemConfig): void {
$cliUpgradeLink = $systemConfig->getValue('upgrade.cli-upgrade-link', '');
$disableWebUpdater = $systemConfig->getValue('upgrade.disable-web', false);
$tooBig = false;
Expand Down Expand Up @@ -1071,106 +1065,115 @@ public static function registerShareHooks(\OC\SystemConfig $systemConfig): void
}

/**
* Handle the request
* Handle the incoming request: bootstrap auth/apps, enforce maintenance/upgrade checks,
* route the request, and fall back to default error or redirect responses.
*/
public static function handleRequest(): void {
Server::get(\OCP\Diagnostics\IEventLogger::class)->start('handle_request', 'Handle request');
$systemConfig = Server::get(\OC\SystemConfig::class);
$installed = $systemConfig->getValue('installed', false);

// Check if Nextcloud is installed or in maintenance (update) mode
if (!$systemConfig->getValue('installed', false)) {
// Run setup if Nextcloud is not installed
if (!$installed) {
Server::get(ISession::class)->clear();
$controller = Server::get(\OC\Core\Controller\SetupController::class);
$controller->run($_POST);
$setup = Server::get(\OC\Core\Controller\SetupController::class);
$setup->run($_POST);
exit();
}

$request = Server::get(IRequest::class);
$request->throwDecodingExceptionIfAny();

$requestPath = $request->getRawPathInfo();

if ($requestPath === '/heartbeat') {
return;
}
if (substr($requestPath, -3) !== '.js') { // we need these files during the upgrade
self::checkMaintenanceMode($systemConfig);

if (\OCP\Util::needUpgrade()) {
if (function_exists('opcache_reset')) {
opcache_reset();
}
if (!((bool)$systemConfig->getValue('maintenance', false))) {
self::printUpgradePage($systemConfig);
exit();
}
$maintenance = (bool)$systemConfig->getValue('maintenance', false);

// Needed during maintenance mode and upgrades
$bypassMaintenance = str_ends_with($requestPath, '.js') || OC::$SUBURI === '/core/ajax/update.php';

// Show "maintenance in progress" page if Nextcloud is undergoing maintenance and not a bypass URL
if ($maintenance && !$bypassMaintenance) {
self::renderMaintenancePage($systemConfig);
exit();
}

$upgrade = Util::needUpgrade();

// Show "upgrade" page if Nextcloud needs to be upgraded and not in maintenance mode (i.e. already in progress).
if ($upgrade && !$maintenance && !$bypassMaintenance) {
if (function_exists('opcache_reset')) {
opcache_reset();
}
// NOTE: This is shown to the first web visitor to land after a code update...
// ...and will continue to be shown to subsequent visitors until the upgrade is
// triggered.
self::renderUpgradePage($systemConfig);
exit();
}

$appManager = Server::get(\OCP\App\IAppManager::class);
//
// At this point the request has passed the install/maintenance/upgrade gates
// or is using a path that is allowed to bypass them.
//

// Always load authentication apps
$appManager->loadApps(['authentication']);
$appManager->loadApps(['extended_authentication']);
$appManager = Server::get(\OCP\App\IAppManager::class);
$userSession = Server::get(IUserSession::class);
$loggedIn = $userSession->isLoggedIn();

// Load minimum set of apps
if (!\OCP\Util::needUpgrade()
&& !((bool)$systemConfig->getValue('maintenance', false))) {
// For logged-in users: Load everything
if (Server::get(IUserSession::class)->isLoggedIn()) {
$appManager->loadApps();
} else {
// For guests: Load only filesystem and logging
$appManager->loadApps(['filesystem', 'logging']);
self::loadAuthenticationApps($appManager);

// Don't try to login when a client is trying to get a OAuth token.
// OAuth needs to support basic auth too, so the login is not valid
// inside Nextcloud and the Login exception would ruin it.
if ($request->getRawPathInfo() !== '/apps/oauth2/api/v1/token') {
try {
self::handleLogin($request);
} catch (DisabledUserException $e) {
// Disabled users would not be seen as logged in and
// trying to log them in would fail, so the login
// exception is ignored for the themed stylesheets and
// images.
if ($request->getRawPathInfo() !== '/apps/theming/theme/default.css'
&& $request->getRawPathInfo() !== '/apps/theming/theme/light.css'
&& $request->getRawPathInfo() !== '/apps/theming/theme/dark.css'
&& $request->getRawPathInfo() !== '/apps/theming/theme/light-highcontrast.css'
&& $request->getRawPathInfo() !== '/apps/theming/theme/dark-highcontrast.css'
&& $request->getRawPathInfo() !== '/apps/theming/theme/opendyslexic.css'
&& $request->getRawPathInfo() !== '/apps/theming/image/background'
&& $request->getRawPathInfo() !== '/apps/theming/image/logo'
&& $request->getRawPathInfo() !== '/apps/theming/image/logoheader'
&& !str_starts_with($request->getRawPathInfo(), '/apps/theming/favicon')
&& !str_starts_with($request->getRawPathInfo(), '/apps/theming/icon')) {
throw $e;
}
}
}
}
if ($loggedIn) {
self::loadAppsForAuthenticatedRequests($appManager);
} else {
self::loadAppsForPreAuthenticationPhase($appManager);
}

if (!self::$CLI) {
// Don't try to log in when a client is trying to get an OAuth token.
// OAuth needs to support basic auth too, so the login is not valid
// inside Nextcloud and the Login exception would ruin it.
$bypassLogin = $requestPath === '/apps/oauth2/api/v1/token';

if (!$loggedIn && !$bypassLogin) {
try {
if (!\OCP\Util::needUpgrade()) {
$appManager->loadApps(['filesystem', 'logging']);
$appManager->loadApps();
// Try normal login
self::handleLogin($request);
$loggedIn = $userSession->isLoggedIn();

// A successful login expands the app set needed during request handling.
if ($loggedIn) {
self::loadAppsForAuthenticatedRequests($appManager);
}
} catch (DisabledUserException $e) {
// Don’t prevent theming asset requests if user is merely disabled.
if (!self::themingAssetRequest($requestPath)) {
throw $e;
}
Server::get(\OC\Route\Router::class)->match($request->getRawPathInfo());
return;
} catch (Symfony\Component\Routing\Exception\ResourceNotFoundException $e) {
//header('HTTP/1.0 404 Not Found');
} catch (Symfony\Component\Routing\Exception\MethodNotAllowedException $e) {
http_response_code(405);
return;
}
}

// Handle WebDAV
if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PROPFIND') {
// not allowed any more to prevent people
// mounting this root directly.
// Users need to mount remote.php/webdav instead.
// Ensure the full app set is loaded before routing.
self::loadAppsForRouting($appManager);

// Try to route the request.
$router = Server::get(\OC\Route\Router::class);
// Note: User may (or may still not) be logged in.
try {
$router->match($requestPath);
return;
} catch (ResourceNotFoundException $e) {
// ...
} catch (MethodNotAllowedException $e) {
http_response_code(405);
return;
}

$webdav = isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PROPFIND';
if ($webdav) {
// Users need to mount remote.php/{webdav, dav} instead.
http_response_code(405);
return;
}
Expand All @@ -1182,31 +1185,30 @@ public static function handleRequest(): void {
return;
}

// Handle resources that can't be found
// This prevents browsers from redirecting to the default page and then
// attempting to parse HTML as CSS and similar.
// Handle requests for select resource types that are unavailable (regardless of reason)
$destinationHeader = $request->getHeader('Sec-Fetch-Dest');
if (in_array($destinationHeader, ['font', 'script', 'style'])) {
// Prevents browsers from redirecting to the default endpoint and attempting
// to parse HTML as CSS, etc.
http_response_code(404);
return;
}

// Redirect to the default app or login only as an entry point
if ($requestPath === '') {
// Someone is logged in
$userSession = Server::get(IUserSession::class);
// Redirect to the default app if visitor is logged in
if ($userSession->isLoggedIn()) {
header('X-User-Id: ' . $userSession->getUser()?->getUID());
header('Location: ' . Server::get(IURLGenerator::class)->linkToDefaultPageUrl());
} else {
// Not handled and not logged in
// Redirect to the login page if visitor is not logged in
header('Location: ' . Server::get(IURLGenerator::class)->linkToRouteAbsolute('core.login.showLoginForm'));
}
return;
}

// Try to send visitor to the Nextcloud 404 page if at all possible
try {
Server::get(\OC\Route\Router::class)->match('/error/404');
$router->match('/error/404');
} catch (\Exception $e) {
if (!$e instanceof MethodNotAllowedException) {
logger('core')->emergency($e->getMessage(), ['exception' => $e]);
Expand All @@ -1220,8 +1222,77 @@ public static function handleRequest(): void {
}
}

private static function themingAssetRequest(string $requestPath): bool {
if ($requestPath === '/apps/theming/theme/default.css'
|| $requestPath === '/apps/theming/theme/light.css'
|| $requestPath === '/apps/theming/theme/dark.css'
|| $requestPath === '/apps/theming/theme/light-highcontrast.css'
|| $requestPath === '/apps/theming/theme/dark-highcontrast.css'
|| $requestPath === '/apps/theming/theme/opendyslexic.css'
|| $requestPath === '/apps/theming/image/background'
|| $requestPath === '/apps/theming/image/logo'
|| $requestPath === '/apps/theming/image/logoheader'
|| str_starts_with($requestPath, '/apps/theming/favicon')
|| str_starts_with($requestPath, '/apps/theming/icon')
) {
return true;
}

return false;
}

/**
* Load authentication apps before the session-dependent phase of request handling.
*/
private static function loadAuthenticationApps(\OCP\App\IAppManager $appManager): void {
// Always load authentication apps
$appManager->loadApps(['authentication']);
$appManager->loadApps(['extended_authentication']);
}

/**
* Load the baseline runtime apps needed before authentication has succeeded.
*/
private static function loadAppsForPreAuthenticationPhase(\OCP\App\IAppManager $appManager): void {
$appManager->loadApps(['filesystem', 'logging']);
}

/**
* Check login: apache auth, auth token, basic auth
* Load the full app set needed for authenticated requests.
*/
private static function loadAppsForAuthenticatedRequests(\OCP\App\IAppManager $appManager): void {
// Note: loadApps() is smart enough to skip any already loaded apps.
$appManager->loadApps();
}

/**
* Ensure the full app set is loaded before route matching so app routes and
* related runtime registrations are available.
*/
private static function loadAppsForRouting(\OCP\App\IAppManager $appManager): void {
// Preserve the historical routing-time load sequence.
$appManager->loadApps(['filesystem', 'logging']);
$appManager->loadApps();
}

/**
* Attempt to authenticate the current request using supported login methods.
*
* Tries, in order, Apache auth, App API auth, token auth, remembered-login
* cookies, and HTTP basic auth. On success, this updates the current user
* session as a side effect. Federation requests are skipped.
*
* Callers typically inspect the resulting session state afterward rather than
* relying on the return value alone.
*
* @return bool True if one of the supported login mechanisms authenticated the
* request; false if no session was established and no login
* exception was raised.
*
* @throws \OC\User\LoginException If an underlying login mechanism rejects or
* aborts the login flow.
* @throws \OC\User\DisabledUserException If authentication is rejected because
* the user account is disabled.
*/
public static function handleLogin(OCP\IRequest $request): bool {
if ($request->getHeader('X-Nextcloud-Federation')) {
Expand Down
Loading