Skip to content
Open
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
6 changes: 6 additions & 0 deletions build/integration/features/maintenance-mode.feature
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,9 @@ Feature: maintenance-mode
Then the HTTP status code should be "503"
Then Maintenance mode is disabled
And the command was successful

Scenario: Accessing a JS asset with maintenance mode enabled
When requesting "/dist/core-maintenance.js" with "GET"
Then the HTTP status code should be "200"
Then Maintenance mode is disabled
And the command was successful
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();
}
private 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');

// 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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if ($upgrade && !$maintenance && !$bypassMaintenance) {
if ($upgrade && !$bypassMaintenance) {

The maintenance check is redundant, we exited earlier if we are in maintenance without bypass.
We may want to rename $bypassMaintenance as it also bypasses upgrade check. Could be $isJavascriptResource or something? Or $bypassMaintenanceAndUpgrade.

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.
Comment on lines +1111 to +1113
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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.
// 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would actually be clearer:

Suggested change
// 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.
// NOTE: This is shown to web visitors after a code update 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) {
// ...
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be replaced by an explanation of why we continue when this exception happens.

} 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;
}
Comment on lines 1197 to 1207
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can’t that be done earlier?
Looks like a loss of time and resources to first run the router and load all apps just to issue a redirection for default route.


// 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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private static function themingAssetRequest(string $requestPath): bool {
private static function isThemingAssetRequest(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