mirror of
https://github.com/nextcloud/server.git
synced 2026-05-15 09:59:47 -04:00
1363 lines
48 KiB
PHP
1363 lines
48 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
/**
|
||
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
|
||
* SPDX-FileCopyrightText: 2013-2016 ownCloud, Inc.
|
||
* SPDX-License-Identifier: AGPL-3.0-only
|
||
*/
|
||
|
||
use OC\Profiler\BuiltInProfiler;
|
||
use OC\Security\CSP\ContentSecurityPolicyNonceManager;
|
||
use OC\Share20\GroupDeletedListener;
|
||
use OC\Share20\Hooks;
|
||
use OC\Share20\UserDeletedListener;
|
||
use OC\Share20\UserRemovedListener;
|
||
use OC\User\DisabledUserException;
|
||
use OCP\EventDispatcher\IEventDispatcher;
|
||
use OCP\Files\Events\BeforeFileSystemSetupEvent;
|
||
use OCP\Group\Events\GroupDeletedEvent;
|
||
use OCP\Group\Events\UserRemovedEvent;
|
||
use OCP\IAppConfig;
|
||
use OCP\IConfig;
|
||
use OCP\IInitialStateService;
|
||
use OCP\ILogger;
|
||
use OCP\IRequest;
|
||
use OCP\ISession;
|
||
use OCP\IURLGenerator;
|
||
use OCP\IUserSession;
|
||
use OCP\Security\Bruteforce\IThrottler;
|
||
use OCP\Server;
|
||
use OCP\Template\ITemplateManager;
|
||
use OCP\User\Events\UserChangedEvent;
|
||
use OCP\User\Events\UserDeletedEvent;
|
||
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';
|
||
|
||
/**
|
||
* Class that is a namespace for all global OC variables
|
||
* No, we can not put this class in its own file because it is used by
|
||
* OC_autoload!
|
||
*/
|
||
class OC {
|
||
/**
|
||
* The installation path for Nextcloud on the server (e.g. /srv/http/nextcloud)
|
||
* @internal Use auto-loaded $serverRoot with DI instead.
|
||
*/
|
||
public static string $SERVERROOT = '';
|
||
/**
|
||
* the current request path relative to the Nextcloud root (e.g. files/index.php)
|
||
*/
|
||
private static string $SUBURI = '';
|
||
/**
|
||
* the Nextcloud root path for http requests (e.g. /nextcloud)
|
||
*/
|
||
public static string $WEBROOT = '';
|
||
/**
|
||
* The installation path array of the apps folder on the server (e.g. /srv/http/nextcloud) 'path' and
|
||
* web path in 'url'
|
||
*/
|
||
public static array $APPSROOTS = [];
|
||
|
||
public static string $configDir;
|
||
|
||
/**
|
||
* requested app
|
||
*/
|
||
public static string $REQUESTEDAPP = '';
|
||
|
||
/**
|
||
* check if Nextcloud runs in cli mode
|
||
*/
|
||
public static bool $CLI = false;
|
||
|
||
public static \Composer\Autoload\ClassLoader $composerAutoloader;
|
||
|
||
public static \OC\Server $server;
|
||
|
||
private static \OC\Config $config;
|
||
|
||
/**
|
||
* @throws \RuntimeException when the 3rdparty directory is missing or
|
||
* the app path list is empty or contains an invalid path
|
||
*/
|
||
public static function initPaths(): void {
|
||
if (defined('PHPUNIT_CONFIG_DIR')) {
|
||
self::$configDir = OC::$SERVERROOT . '/' . PHPUNIT_CONFIG_DIR . '/';
|
||
} elseif (defined('PHPUNIT_RUN') && PHPUNIT_RUN && is_dir(OC::$SERVERROOT . '/tests/config/')) {
|
||
self::$configDir = OC::$SERVERROOT . '/tests/config/';
|
||
} elseif ($dir = getenv('NEXTCLOUD_CONFIG_DIR')) {
|
||
self::$configDir = rtrim($dir, '/') . '/';
|
||
} else {
|
||
self::$configDir = OC::$SERVERROOT . '/config/';
|
||
}
|
||
self::$config = new \OC\Config(self::$configDir);
|
||
|
||
OC::$SUBURI = str_replace('\\', '/', substr(realpath($_SERVER['SCRIPT_FILENAME'] ?? ''), strlen(OC::$SERVERROOT)));
|
||
/**
|
||
* FIXME: The following lines are required because we can't yet instantiate
|
||
* Server::get(\OCP\IRequest::class) since \OC::$server does not yet exist.
|
||
*/
|
||
$params = [
|
||
'server' => [
|
||
'SCRIPT_NAME' => $_SERVER['SCRIPT_NAME'] ?? null,
|
||
'SCRIPT_FILENAME' => $_SERVER['SCRIPT_FILENAME'] ?? null,
|
||
],
|
||
];
|
||
if (isset($_SERVER['REMOTE_ADDR'])) {
|
||
$params['server']['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'];
|
||
}
|
||
$fakeRequest = new \OC\AppFramework\Http\Request(
|
||
$params,
|
||
new \OC\AppFramework\Http\RequestId($_SERVER['UNIQUE_ID'] ?? '', new \OC\Security\SecureRandom()),
|
||
new \OC\AllConfig(new \OC\SystemConfig(self::$config))
|
||
);
|
||
$scriptName = $fakeRequest->getScriptName();
|
||
if (substr($scriptName, -1) == '/') {
|
||
$scriptName .= 'index.php';
|
||
//make sure suburi follows the same rules as scriptName
|
||
if (substr(OC::$SUBURI, -9) !== 'index.php') {
|
||
if (substr(OC::$SUBURI, -1) !== '/') {
|
||
OC::$SUBURI = OC::$SUBURI . '/';
|
||
}
|
||
OC::$SUBURI = OC::$SUBURI . 'index.php';
|
||
}
|
||
}
|
||
|
||
if (OC::$CLI) {
|
||
OC::$WEBROOT = self::$config->getValue('overwritewebroot', '');
|
||
} else {
|
||
if (substr($scriptName, 0 - strlen(OC::$SUBURI)) === OC::$SUBURI) {
|
||
OC::$WEBROOT = substr($scriptName, 0, 0 - strlen(OC::$SUBURI));
|
||
|
||
if (OC::$WEBROOT !== '' && OC::$WEBROOT[0] !== '/') {
|
||
OC::$WEBROOT = '/' . OC::$WEBROOT;
|
||
}
|
||
} else {
|
||
// The scriptName is not ending with OC::$SUBURI
|
||
// This most likely means that we are calling from CLI.
|
||
// However some cron jobs still need to generate
|
||
// a web URL, so we use overwritewebroot as a fallback.
|
||
OC::$WEBROOT = self::$config->getValue('overwritewebroot', '');
|
||
}
|
||
|
||
// Resolve /nextcloud to /nextcloud/ to ensure to always have a trailing
|
||
// slash which is required by URL generation.
|
||
if (isset($_SERVER['REQUEST_URI']) && $_SERVER['REQUEST_URI'] === \OC::$WEBROOT
|
||
&& substr($_SERVER['REQUEST_URI'], -1) !== '/') {
|
||
header('Location: ' . \OC::$WEBROOT . '/');
|
||
exit();
|
||
}
|
||
}
|
||
|
||
// search the apps folder
|
||
$config_paths = self::$config->getValue('apps_paths', []);
|
||
if (!empty($config_paths)) {
|
||
foreach ($config_paths as $paths) {
|
||
if (isset($paths['url']) && isset($paths['path'])) {
|
||
$paths['url'] = rtrim($paths['url'], '/');
|
||
$paths['path'] = rtrim($paths['path'], '/');
|
||
OC::$APPSROOTS[] = $paths;
|
||
}
|
||
}
|
||
} elseif (file_exists(OC::$SERVERROOT . '/apps')) {
|
||
OC::$APPSROOTS[] = ['path' => OC::$SERVERROOT . '/apps', 'url' => '/apps', 'writable' => true];
|
||
}
|
||
|
||
if (empty(OC::$APPSROOTS)) {
|
||
throw new \RuntimeException('apps directory not found! Please put the Nextcloud apps folder in the Nextcloud folder'
|
||
. '. You can also configure the location in the config.php file.');
|
||
}
|
||
$paths = [];
|
||
foreach (OC::$APPSROOTS as $path) {
|
||
$paths[] = $path['path'];
|
||
if (!is_dir($path['path'])) {
|
||
throw new \RuntimeException(sprintf('App directory "%s" not found! Please put the Nextcloud apps folder in the'
|
||
. ' Nextcloud folder. You can also configure the location in the config.php file.', $path['path']));
|
||
}
|
||
}
|
||
|
||
// set the right include path
|
||
set_include_path(
|
||
implode(PATH_SEPARATOR, $paths)
|
||
);
|
||
}
|
||
|
||
public static function checkConfig(): void {
|
||
// Create config if it does not already exist
|
||
$configFilePath = self::$configDir . '/config.php';
|
||
if (!file_exists($configFilePath)) {
|
||
@touch($configFilePath);
|
||
}
|
||
|
||
// Check if config is writable
|
||
$configFileWritable = is_writable($configFilePath);
|
||
$configReadOnly = Server::get(IConfig::class)->getSystemValueBool('config_is_read_only');
|
||
if (!$configFileWritable && !$configReadOnly
|
||
|| !$configFileWritable && \OCP\Util::needUpgrade()) {
|
||
$urlGenerator = Server::get(IURLGenerator::class);
|
||
$l = Server::get(\OCP\L10N\IFactory::class)->get('lib');
|
||
|
||
if (self::$CLI) {
|
||
echo $l->t('Cannot write into "config" directory!') . "\n";
|
||
echo $l->t('This can usually be fixed by giving the web server write access to the config directory.') . "\n";
|
||
echo "\n";
|
||
echo $l->t('But, if you prefer to keep config.php file read only, set the option "config_is_read_only" to true in it.') . "\n";
|
||
echo $l->t('See %s', [ $urlGenerator->linkToDocs('admin-config') ]) . "\n";
|
||
exit;
|
||
} else {
|
||
Server::get(ITemplateManager::class)->printErrorPage(
|
||
$l->t('Cannot write into "config" directory!'),
|
||
$l->t('This can usually be fixed by giving the web server write access to the config directory.') . ' '
|
||
. $l->t('But, if you prefer to keep config.php file read only, set the option "config_is_read_only" to true in it.') . ' '
|
||
. $l->t('See %s', [ $urlGenerator->linkToDocs('admin-config') ]),
|
||
503
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
public static function checkInstalled(\OC\SystemConfig $systemConfig): void {
|
||
if (defined('OC_CONSOLE')) {
|
||
return;
|
||
}
|
||
// Redirect to installer if not installed
|
||
if (!$systemConfig->getValue('installed', false) && OC::$SUBURI !== '/index.php' && OC::$SUBURI !== '/status.php') {
|
||
if (OC::$CLI) {
|
||
throw new Exception('Not installed');
|
||
} else {
|
||
$url = OC::$WEBROOT . '/index.php';
|
||
header('Location: ' . $url);
|
||
}
|
||
exit();
|
||
}
|
||
}
|
||
|
||
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 renderUpgradePage(\OC\SystemConfig $systemConfig): void {
|
||
$cliUpgradeLink = $systemConfig->getValue('upgrade.cli-upgrade-link', '');
|
||
$disableWebUpdater = $systemConfig->getValue('upgrade.disable-web', false);
|
||
$tooBig = false;
|
||
if (!$disableWebUpdater) {
|
||
$apps = Server::get(\OCP\App\IAppManager::class);
|
||
if ($apps->isEnabledForAnyone('user_ldap')) {
|
||
$qb = Server::get(\OCP\IDBConnection::class)->getQueryBuilder();
|
||
|
||
$result = $qb->select($qb->func()->count('*', 'user_count'))
|
||
->from('ldap_user_mapping')
|
||
->executeQuery();
|
||
$row = $result->fetch();
|
||
$result->closeCursor();
|
||
|
||
$tooBig = ($row['user_count'] > 50);
|
||
}
|
||
if (!$tooBig && $apps->isEnabledForAnyone('user_saml')) {
|
||
$qb = Server::get(\OCP\IDBConnection::class)->getQueryBuilder();
|
||
|
||
$result = $qb->select($qb->func()->count('*', 'user_count'))
|
||
->from('user_saml_users')
|
||
->executeQuery();
|
||
$row = $result->fetch();
|
||
$result->closeCursor();
|
||
|
||
$tooBig = ($row['user_count'] > 50);
|
||
}
|
||
if (!$tooBig) {
|
||
// count users
|
||
$totalUsers = Server::get(\OCP\IUserManager::class)->countUsersTotal(51);
|
||
$tooBig = ($totalUsers > 50);
|
||
}
|
||
}
|
||
$ignoreTooBigWarning = isset($_GET['IKnowThatThisIsABigInstanceAndTheUpdateRequestCouldRunIntoATimeoutAndHowToRestoreABackup'])
|
||
&& $_GET['IKnowThatThisIsABigInstanceAndTheUpdateRequestCouldRunIntoATimeoutAndHowToRestoreABackup'] === 'IAmSuperSureToDoThis';
|
||
|
||
Util::addTranslations('core');
|
||
Util::addScript('core', 'common');
|
||
Util::addScript('core', 'main');
|
||
Util::addScript('core', 'update');
|
||
|
||
$initialState = Server::get(IInitialStateService::class);
|
||
$serverVersion = Server::get(\OCP\ServerVersion::class);
|
||
if ($disableWebUpdater || ($tooBig && !$ignoreTooBigWarning)) {
|
||
// send http status 503
|
||
http_response_code(503);
|
||
header('Retry-After: 120');
|
||
|
||
$urlGenerator = Server::get(IURLGenerator::class);
|
||
$initialState->provideInitialState('core', 'updaterView', 'adminCli');
|
||
$initialState->provideInitialState('core', 'updateInfo', [
|
||
'cliUpgradeLink' => $cliUpgradeLink ?: $urlGenerator->linkToDocs('admin-cli-upgrade'),
|
||
'productName' => self::getProductName(),
|
||
'version' => $serverVersion->getVersionString(),
|
||
'tooBig' => $tooBig,
|
||
]);
|
||
|
||
// render error page
|
||
Server::get(ITemplateManager::class)
|
||
->getTemplate('', 'update', 'guest')
|
||
->printPage();
|
||
die();
|
||
}
|
||
|
||
// check whether this is a core update or apps update
|
||
$installedVersion = $systemConfig->getValue('version', '0.0.0');
|
||
$currentVersion = implode('.', $serverVersion->getVersion());
|
||
|
||
// if not a core upgrade, then it's apps upgrade
|
||
$isAppsOnlyUpgrade = version_compare($currentVersion, $installedVersion, '=');
|
||
|
||
$oldTheme = $systemConfig->getValue('theme');
|
||
$systemConfig->setValue('theme', '');
|
||
|
||
/** @var \OC\App\AppManager $appManager */
|
||
$appManager = Server::get(\OCP\App\IAppManager::class);
|
||
|
||
// get third party apps
|
||
$ocVersion = $serverVersion->getVersion();
|
||
$ocVersion = implode('.', $ocVersion);
|
||
$incompatibleApps = $appManager->getIncompatibleApps($ocVersion);
|
||
$incompatibleOverwrites = $systemConfig->getValue('app_install_overwrite', []);
|
||
$incompatibleShippedApps = [];
|
||
$incompatibleDisabledApps = [];
|
||
foreach ($incompatibleApps as $appInfo) {
|
||
if ($appManager->isShipped($appInfo['id'])) {
|
||
$incompatibleShippedApps[] = $appInfo['name'] . ' (' . $appInfo['id'] . ')';
|
||
}
|
||
if (!in_array($appInfo['id'], $incompatibleOverwrites)) {
|
||
$incompatibleDisabledApps[] = $appInfo;
|
||
}
|
||
}
|
||
|
||
if (!empty($incompatibleShippedApps)) {
|
||
$l = Server::get(\OCP\L10N\IFactory::class)->get('core');
|
||
$hint = $l->t('Application %1$s is not present or has a non-compatible version with this server. Please check the apps directory.', [implode(', ', $incompatibleShippedApps)]);
|
||
throw new \OCP\HintException('Application ' . implode(', ', $incompatibleShippedApps) . ' is not present or has a non-compatible version with this server. Please check the apps directory.', $hint);
|
||
}
|
||
|
||
$appConfig = Server::get(IAppConfig::class);
|
||
$appsToUpgrade = array_map(function ($app) use (&$appConfig) {
|
||
return [
|
||
'id' => $app['id'],
|
||
'name' => $app['name'],
|
||
'version' => $app['version'],
|
||
'oldVersion' => $appConfig->getValueString($app['id'], 'installed_version'),
|
||
];
|
||
}, $appManager->getAppsNeedingUpgrade($ocVersion));
|
||
|
||
$params = [
|
||
'appsToUpgrade' => $appsToUpgrade,
|
||
'incompatibleAppsList' => $incompatibleDisabledApps,
|
||
'isAppsOnlyUpgrade' => $isAppsOnlyUpgrade,
|
||
'oldTheme' => $oldTheme,
|
||
'productName' => self::getProductName(),
|
||
'version' => $serverVersion->getVersionString(),
|
||
];
|
||
|
||
$initialState->provideInitialState('core', 'updaterView', 'admin');
|
||
$initialState->provideInitialState('core', 'updateInfo', $params);
|
||
Server::get(ITemplateManager::class)
|
||
->getTemplate('', 'update', 'guest')
|
||
->printPage();
|
||
}
|
||
|
||
private static function getProductName(): string {
|
||
$productName = 'Nextcloud';
|
||
try {
|
||
$defaults = new \OC_Defaults();
|
||
$productName = $defaults->getName();
|
||
} catch (Throwable $error) {
|
||
// ignore
|
||
}
|
||
return $productName;
|
||
}
|
||
|
||
public static function initSession(): void {
|
||
$request = Server::get(IRequest::class);
|
||
|
||
// TODO: Temporary disabled again to solve issues with CalDAV/CardDAV clients like DAVx5 that use cookies
|
||
// TODO: See https://github.com/nextcloud/server/issues/37277#issuecomment-1476366147 and the other comments
|
||
// TODO: for further information.
|
||
// $isDavRequest = strpos($request->getRequestUri(), '/remote.php/dav') === 0 || strpos($request->getRequestUri(), '/remote.php/webdav') === 0;
|
||
// if ($request->getHeader('Authorization') !== '' && is_null($request->getCookie('cookie_test')) && $isDavRequest && !isset($_COOKIE['nc_session_id'])) {
|
||
// setcookie('cookie_test', 'test', time() + 3600);
|
||
// // Do not initialize the session if a request is authenticated directly
|
||
// // unless there is a session cookie already sent along
|
||
// return;
|
||
// }
|
||
|
||
if ($request->getServerProtocol() === 'https') {
|
||
ini_set('session.cookie_secure', 'true');
|
||
}
|
||
|
||
// prevents javascript from accessing php session cookies
|
||
ini_set('session.cookie_httponly', 'true');
|
||
|
||
// set the cookie path to the Nextcloud directory
|
||
$cookie_path = OC::$WEBROOT ? : '/';
|
||
ini_set('session.cookie_path', $cookie_path);
|
||
|
||
// set the cookie domain to the Nextcloud domain
|
||
$cookie_domain = self::$config->getValue('cookie_domain', '');
|
||
if ($cookie_domain) {
|
||
ini_set('session.cookie_domain', $cookie_domain);
|
||
}
|
||
|
||
// Do not initialize sessions for 'status.php' requests
|
||
// Monitoring endpoints can quickly flood session handlers
|
||
// and 'status.php' doesn't require sessions anyway
|
||
// We still need to run the ini_set above so that same-site cookies use the correct configuration.
|
||
if (str_ends_with($request->getScriptName(), '/status.php')) {
|
||
return;
|
||
}
|
||
|
||
// Let the session name be changed in the initSession Hook
|
||
$sessionName = OC_Util::getInstanceId();
|
||
|
||
try {
|
||
$logger = null;
|
||
if (Server::get(\OC\SystemConfig::class)->getValue('installed', false)) {
|
||
$logger = logger('core');
|
||
}
|
||
|
||
// set the session name to the instance id - which is unique
|
||
$session = new \OC\Session\Internal(
|
||
$sessionName,
|
||
$logger,
|
||
);
|
||
|
||
$cryptoWrapper = Server::get(\OC\Session\CryptoWrapper::class);
|
||
$session = $cryptoWrapper->wrapSession($session);
|
||
self::$server->setSession($session);
|
||
|
||
// if session can't be started break with http 500 error
|
||
} catch (Exception $e) {
|
||
Server::get(LoggerInterface::class)->error($e->getMessage(), ['app' => 'base','exception' => $e]);
|
||
//show the user a detailed error page
|
||
Server::get(ITemplateManager::class)->printExceptionErrorPage($e, 500);
|
||
die();
|
||
}
|
||
|
||
//try to set the session lifetime
|
||
$sessionLifeTime = self::getSessionLifeTime();
|
||
|
||
// session timeout
|
||
if ($session->exists('LAST_ACTIVITY') && (time() - $session->get('LAST_ACTIVITY') > $sessionLifeTime)) {
|
||
if (isset($_COOKIE[session_name()])) {
|
||
setcookie(session_name(), '', -1, self::$WEBROOT ? : '/');
|
||
}
|
||
Server::get(IUserSession::class)->logout();
|
||
}
|
||
|
||
if (!self::hasSessionRelaxedExpiry()) {
|
||
$session->set('LAST_ACTIVITY', time());
|
||
}
|
||
$session->close();
|
||
}
|
||
|
||
private static function getSessionLifeTime(): int {
|
||
return Server::get(IConfig::class)->getSystemValueInt('session_lifetime', 60 * 60 * 24);
|
||
}
|
||
|
||
/**
|
||
* @return bool true if the session expiry should only be done by gc instead of an explicit timeout
|
||
*/
|
||
public static function hasSessionRelaxedExpiry(): bool {
|
||
return Server::get(IConfig::class)->getSystemValueBool('session_relaxed_expiry', false);
|
||
}
|
||
|
||
/**
|
||
* Try to set some values to the required Nextcloud default
|
||
*/
|
||
public static function setRequiredIniValues(): void {
|
||
// Don't display errors and log them
|
||
@ini_set('display_errors', '0');
|
||
@ini_set('log_errors', '1');
|
||
|
||
// Try to configure php to enable big file uploads.
|
||
// This doesn't work always depending on the webserver and php configuration.
|
||
// Let's try to overwrite some defaults if they are smaller than 1 hour
|
||
|
||
if (intval(@ini_get('max_execution_time') ?: 0) < 3600) {
|
||
@ini_set('max_execution_time', strval(3600));
|
||
}
|
||
|
||
if (intval(@ini_get('max_input_time') ?: 0) < 3600) {
|
||
@ini_set('max_input_time', strval(3600));
|
||
}
|
||
|
||
// Try to set the maximum execution time to the largest time limit we have
|
||
if (strpos(@ini_get('disable_functions'), 'set_time_limit') === false) {
|
||
@set_time_limit(max(intval(@ini_get('max_execution_time')), intval(@ini_get('max_input_time'))));
|
||
}
|
||
|
||
@ini_set('default_charset', 'UTF-8');
|
||
@ini_set('gd.jpeg_ignore_warning', '1');
|
||
}
|
||
|
||
/**
|
||
* Send the same site cookies
|
||
*/
|
||
private static function sendSameSiteCookies(): void {
|
||
$cookieParams = session_get_cookie_params();
|
||
$secureCookie = ($cookieParams['secure'] === true) ? 'secure; ' : '';
|
||
$policies = [
|
||
'lax',
|
||
'strict',
|
||
];
|
||
|
||
// Append __Host to the cookie if it meets the requirements
|
||
$cookiePrefix = '';
|
||
if ($cookieParams['secure'] === true && $cookieParams['path'] === '/') {
|
||
$cookiePrefix = '__Host-';
|
||
}
|
||
|
||
foreach ($policies as $policy) {
|
||
header(
|
||
sprintf(
|
||
'Set-Cookie: %snc_sameSiteCookie%s=true; path=%s; httponly;' . $secureCookie . 'expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=%s',
|
||
$cookiePrefix,
|
||
$policy,
|
||
$cookieParams['path'],
|
||
$policy
|
||
),
|
||
false
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Same Site cookie to further mitigate CSRF attacks. This cookie has to
|
||
* be set in every request if cookies are sent to add a second level of
|
||
* defense against CSRF.
|
||
*
|
||
* If the cookie is not sent this will set the cookie and reload the page.
|
||
* We use an additional cookie since we want to protect logout CSRF and
|
||
* also we can't directly interfere with PHP's session mechanism.
|
||
*/
|
||
private static function performSameSiteCookieProtection(IConfig $config): void {
|
||
$request = Server::get(IRequest::class);
|
||
|
||
// Some user agents are notorious and don't really properly follow HTTP
|
||
// specifications. For those, have an automated opt-out. Since the protection
|
||
// for remote.php is applied in base.php as starting point we need to opt out
|
||
// here.
|
||
$incompatibleUserAgents = $config->getSystemValue('csrf.optout');
|
||
|
||
// Fallback, if csrf.optout is unset
|
||
if (!is_array($incompatibleUserAgents)) {
|
||
$incompatibleUserAgents = [
|
||
// OS X Finder
|
||
'/^WebDAVFS/',
|
||
// Windows webdav drive
|
||
'/^Microsoft-WebDAV-MiniRedir/',
|
||
];
|
||
}
|
||
|
||
if ($request->isUserAgent($incompatibleUserAgents)) {
|
||
return;
|
||
}
|
||
|
||
if (count($_COOKIE) > 0) {
|
||
$requestUri = $request->getScriptName();
|
||
$processingScript = explode('/', $requestUri);
|
||
$processingScript = $processingScript[count($processingScript) - 1];
|
||
|
||
if ($processingScript === 'index.php' // index.php routes are handled in the middleware
|
||
|| $processingScript === 'cron.php' // and cron.php does not need any authentication at all
|
||
|| $processingScript === 'public.php' // For public.php, auth for password protected shares is done in the PublicAuth plugin
|
||
) {
|
||
return;
|
||
}
|
||
|
||
// All other endpoints require the lax and the strict cookie
|
||
if (!$request->passesStrictCookieCheck()) {
|
||
logger('core')->warning('Request does not pass strict cookie check');
|
||
self::sendSameSiteCookies();
|
||
// Debug mode gets access to the resources without strict cookie
|
||
// due to the fact that the SabreDAV browser also lives there.
|
||
if (!$config->getSystemValueBool('debug', false)) {
|
||
http_response_code(\OCP\AppFramework\Http::STATUS_PRECONDITION_FAILED);
|
||
header('Content-Type: application/json');
|
||
echo json_encode(['error' => 'Strict Cookie has not been found in request']);
|
||
exit();
|
||
}
|
||
}
|
||
} elseif (!isset($_COOKIE['nc_sameSiteCookielax']) || !isset($_COOKIE['nc_sameSiteCookiestrict'])) {
|
||
self::sendSameSiteCookies();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* This function adds some security related headers to all requests served via base.php
|
||
* The implementation of this function has to happen here to ensure that all third-party
|
||
* components (e.g. SabreDAV) also benefit from this headers.
|
||
*/
|
||
private static function addSecurityHeaders(): void {
|
||
/**
|
||
* FIXME: Content Security Policy for legacy components. This
|
||
* can be removed once \OCP\AppFramework\Http\Response from the AppFramework
|
||
* is used everywhere.
|
||
* @see \OCP\AppFramework\Http\Response::getHeaders
|
||
*/
|
||
$policy = 'default-src \'self\'; '
|
||
. 'script-src \'self\' \'nonce-' . Server::get(ContentSecurityPolicyNonceManager::class)->getNonce() . '\'; '
|
||
. 'style-src \'self\' \'unsafe-inline\'; '
|
||
. 'frame-src *; '
|
||
. 'img-src * data: blob:; '
|
||
. 'font-src \'self\' data:; '
|
||
. 'media-src *; '
|
||
. 'connect-src *; '
|
||
. 'object-src \'none\'; '
|
||
. 'base-uri \'self\'; ';
|
||
header('Content-Security-Policy:' . $policy);
|
||
|
||
// Send fallback headers for installations that don't have the possibility to send
|
||
// custom headers on the webserver side
|
||
if (getenv('modHeadersAvailable') !== 'true') {
|
||
header('Referrer-Policy: no-referrer'); // https://www.w3.org/TR/referrer-policy/
|
||
header('X-Content-Type-Options: nosniff'); // Disable sniffing the content type for IE
|
||
header('X-Frame-Options: SAMEORIGIN'); // Disallow iFraming from other domains
|
||
header('X-Permitted-Cross-Domain-Policies: none'); // https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html
|
||
header('X-Robots-Tag: noindex, nofollow'); // https://developers.google.com/webmasters/control-crawl-index/docs/robots_meta_tag
|
||
}
|
||
}
|
||
|
||
public static function init(): void {
|
||
// First handle PHP configuration and copy auth headers to the expected
|
||
// $_SERVER variable before doing anything Server object related
|
||
self::setRequiredIniValues();
|
||
self::handleAuthHeaders();
|
||
|
||
// prevent any XML processing from loading external entities
|
||
libxml_set_external_entity_loader(static function () {
|
||
return null;
|
||
});
|
||
|
||
// Set default timezone before the Server object is booted
|
||
if (!date_default_timezone_set('UTC')) {
|
||
throw new \RuntimeException('Could not set timezone to UTC');
|
||
}
|
||
|
||
// calculate the root directories
|
||
OC::$SERVERROOT = str_replace('\\', '/', substr(__DIR__, 0, -4));
|
||
|
||
// register autoloader
|
||
$loaderStart = microtime(true);
|
||
|
||
self::$CLI = (php_sapi_name() == 'cli');
|
||
|
||
// Add default composer PSR-4 autoloader, ensure apcu to be disabled
|
||
self::$composerAutoloader = require_once OC::$SERVERROOT . '/lib/composer/autoload.php';
|
||
self::$composerAutoloader->setApcuPrefix(null);
|
||
|
||
|
||
try {
|
||
self::initPaths();
|
||
// setup 3rdparty autoloader
|
||
$vendorAutoLoad = OC::$SERVERROOT . '/3rdparty/autoload.php';
|
||
if (!file_exists($vendorAutoLoad)) {
|
||
throw new \RuntimeException('Composer autoloader not found, unable to continue. Check the folder "3rdparty". Running "git submodule update --init" will initialize the git submodule that handles the subfolder "3rdparty".');
|
||
}
|
||
require_once $vendorAutoLoad;
|
||
} catch (\RuntimeException $e) {
|
||
if (!self::$CLI) {
|
||
http_response_code(503);
|
||
}
|
||
// we can't use the template error page here, because this needs the
|
||
// DI container which isn't available yet
|
||
print($e->getMessage());
|
||
exit();
|
||
}
|
||
$loaderEnd = microtime(true);
|
||
|
||
// Enable lazy loading if activated
|
||
\OC\AppFramework\Utility\SimpleContainer::$useLazyObjects = (bool)self::$config->getValue('enable_lazy_objects', true);
|
||
|
||
// setup the basic server
|
||
self::$server = new \OC\Server(\OC::$WEBROOT, self::$config);
|
||
self::$server->boot();
|
||
|
||
try {
|
||
$profiler = new BuiltInProfiler(
|
||
Server::get(IConfig::class),
|
||
Server::get(IRequest::class),
|
||
);
|
||
$profiler->start();
|
||
} catch (\Throwable $e) {
|
||
logger('core')->error('Failed to start profiler: ' . $e->getMessage(), ['app' => 'base']);
|
||
}
|
||
|
||
if (self::$CLI && in_array('--' . \OCP\Console\ReservedOptions::DEBUG_LOG, $_SERVER['argv'])) {
|
||
\OC\Core\Listener\BeforeMessageLoggedEventListener::setup();
|
||
}
|
||
|
||
$eventLogger = Server::get(\OCP\Diagnostics\IEventLogger::class);
|
||
$eventLogger->log('autoloader', 'Autoloader', $loaderStart, $loaderEnd);
|
||
$eventLogger->start('boot', 'Initialize');
|
||
|
||
// Override php.ini and log everything if we're troubleshooting
|
||
if (self::$config->getValue('loglevel') === ILogger::DEBUG) {
|
||
error_reporting(E_ALL);
|
||
}
|
||
|
||
// initialize intl fallback if necessary
|
||
OC_Util::isSetLocaleWorking();
|
||
|
||
$config = Server::get(IConfig::class);
|
||
if (!defined('PHPUNIT_RUN')) {
|
||
$errorHandler = new OC\Log\ErrorHandler(
|
||
Server::get(\Psr\Log\LoggerInterface::class),
|
||
);
|
||
$exceptionHandler = [$errorHandler, 'onException'];
|
||
if ($config->getSystemValueBool('debug', false)) {
|
||
set_error_handler([$errorHandler, 'onAll'], E_ALL);
|
||
if (\OC::$CLI) {
|
||
$exceptionHandler = [Server::get(ITemplateManager::class), 'printExceptionErrorPage'];
|
||
}
|
||
} else {
|
||
set_error_handler([$errorHandler, 'onError']);
|
||
}
|
||
register_shutdown_function([$errorHandler, 'onShutdown']);
|
||
set_exception_handler($exceptionHandler);
|
||
}
|
||
|
||
/** @var \OC\AppFramework\Bootstrap\Coordinator $bootstrapCoordinator */
|
||
$bootstrapCoordinator = Server::get(\OC\AppFramework\Bootstrap\Coordinator::class);
|
||
$bootstrapCoordinator->runInitialRegistration();
|
||
|
||
$eventLogger->start('init_session', 'Initialize session');
|
||
|
||
// Check for PHP SimpleXML extension earlier since we need it before our other checks and want to provide a useful hint for web users
|
||
// see https://github.com/nextcloud/server/pull/2619
|
||
if (!function_exists('simplexml_load_file')) {
|
||
throw new \OCP\HintException('The PHP SimpleXML/PHP-XML extension is not installed.', 'Install the extension or make sure it is enabled.');
|
||
}
|
||
|
||
$systemConfig = Server::get(\OC\SystemConfig::class);
|
||
$appManager = Server::get(\OCP\App\IAppManager::class);
|
||
if ($systemConfig->getValue('installed', false)) {
|
||
$appManager->loadApps(['session']);
|
||
}
|
||
if (!self::$CLI) {
|
||
self::initSession();
|
||
}
|
||
$eventLogger->end('init_session');
|
||
self::checkConfig();
|
||
self::checkInstalled($systemConfig);
|
||
|
||
if (!self::$CLI) {
|
||
self::addSecurityHeaders();
|
||
self::performSameSiteCookieProtection($config);
|
||
}
|
||
|
||
if (!defined('OC_CONSOLE')) {
|
||
$eventLogger->start('check_server', 'Run a few configuration checks');
|
||
$errors = OC_Util::checkServer($systemConfig);
|
||
if (count($errors) > 0) {
|
||
if (!self::$CLI) {
|
||
http_response_code(503);
|
||
Util::addStyle('guest');
|
||
try {
|
||
Server::get(ITemplateManager::class)->printGuestPage('', 'error', ['errors' => $errors]);
|
||
exit;
|
||
} catch (\Exception $e) {
|
||
// In case any error happens when showing the error page, we simply fall back to posting the text.
|
||
// This might be the case when e.g. the data directory is broken and we can not load/write SCSS to/from it.
|
||
}
|
||
}
|
||
|
||
// Convert l10n string into regular string for usage in database
|
||
$staticErrors = [];
|
||
foreach ($errors as $error) {
|
||
echo $error['error'] . "\n";
|
||
echo $error['hint'] . "\n\n";
|
||
$staticErrors[] = [
|
||
'error' => (string)$error['error'],
|
||
'hint' => (string)$error['hint'],
|
||
];
|
||
}
|
||
|
||
try {
|
||
$config->setAppValue('core', 'cronErrors', json_encode($staticErrors));
|
||
} catch (\Exception $e) {
|
||
echo('Writing to database failed');
|
||
}
|
||
exit(1);
|
||
} elseif (self::$CLI && $config->getSystemValueBool('installed', false)) {
|
||
$config->deleteAppValue('core', 'cronErrors');
|
||
}
|
||
$eventLogger->end('check_server');
|
||
}
|
||
|
||
// User and Groups
|
||
if (!$systemConfig->getValue('installed', false)) {
|
||
Server::get(ISession::class)->set('user_id', '');
|
||
}
|
||
|
||
$eventLogger->start('setup_backends', 'Setup group and user backends');
|
||
Server::get(\OCP\IUserManager::class)->registerBackend(new \OC\User\Database());
|
||
Server::get(\OCP\IGroupManager::class)->addBackend(new \OC\Group\Database());
|
||
|
||
// Subscribe to the hook
|
||
\OCP\Util::connectHook(
|
||
'\OCA\Files_Sharing\API\Server2Server',
|
||
'preLoginNameUsedAsUserName',
|
||
'\OC\User\Database',
|
||
'preLoginNameUsedAsUserName'
|
||
);
|
||
|
||
//setup extra user backends
|
||
if (!\OCP\Util::needUpgrade()) {
|
||
OC_User::setupBackends();
|
||
} else {
|
||
// Run upgrades in incognito mode
|
||
OC_User::setIncognitoMode(true);
|
||
}
|
||
$eventLogger->end('setup_backends');
|
||
|
||
self::registerCleanupHooks($systemConfig);
|
||
self::registerShareHooks($systemConfig);
|
||
self::registerEncryptionWrapperAndHooks();
|
||
self::registerAccountHooks();
|
||
self::registerResourceCollectionHooks();
|
||
self::registerFileReferenceEventListener();
|
||
self::registerRenderReferenceEventListener();
|
||
self::registerAppRestrictionsHooks();
|
||
|
||
// Make sure that the application class is not loaded before the database is setup
|
||
if ($systemConfig->getValue('installed', false)) {
|
||
$appManager->loadApp('settings');
|
||
}
|
||
|
||
//make sure temporary files are cleaned up
|
||
$tmpManager = Server::get(\OCP\ITempManager::class);
|
||
register_shutdown_function([$tmpManager, 'clean']);
|
||
$lockProvider = Server::get(\OCP\Lock\ILockingProvider::class);
|
||
register_shutdown_function([$lockProvider, 'releaseAll']);
|
||
|
||
// Check whether the sample configuration has been copied
|
||
if ($systemConfig->getValue('copied_sample_config', false)) {
|
||
$l = Server::get(\OCP\L10N\IFactory::class)->get('lib');
|
||
Server::get(ITemplateManager::class)->printErrorPage(
|
||
$l->t('Sample configuration detected'),
|
||
$l->t('It has been detected that the sample configuration has been copied. This can break your installation and is unsupported. Please read the documentation before performing changes on config.php'),
|
||
503
|
||
);
|
||
return;
|
||
}
|
||
|
||
$request = Server::get(IRequest::class);
|
||
$host = $request->getInsecureServerHost();
|
||
/**
|
||
* if the host passed in headers isn't trusted
|
||
* FIXME: Should not be in here at all :see_no_evil:
|
||
*/
|
||
if (!OC::$CLI
|
||
&& !Server::get(\OC\Security\TrustedDomainHelper::class)->isTrustedDomain($host)
|
||
&& $config->getSystemValueBool('installed', false)
|
||
) {
|
||
// Allow access to CSS resources
|
||
$isScssRequest = false;
|
||
if (strpos($request->getPathInfo() ?: '', '/css/') === 0) {
|
||
$isScssRequest = true;
|
||
}
|
||
|
||
if (substr($request->getRequestUri(), -11) === '/status.php') {
|
||
http_response_code(400);
|
||
header('Content-Type: application/json');
|
||
echo '{"error": "Trusted domain error.", "code": 15}';
|
||
exit();
|
||
}
|
||
|
||
if (!$isScssRequest) {
|
||
http_response_code(400);
|
||
Server::get(LoggerInterface::class)->info(
|
||
'Trusted domain error. "{remoteAddress}" tried to access using "{host}" as host.',
|
||
[
|
||
'app' => 'core',
|
||
'remoteAddress' => $request->getRemoteAddress(),
|
||
'host' => $host,
|
||
]
|
||
);
|
||
|
||
$tmpl = Server::get(ITemplateManager::class)->getTemplate('core', 'untrustedDomain', 'guest');
|
||
$tmpl->assign('docUrl', Server::get(IURLGenerator::class)->linkToDocs('admin-trusted-domains'));
|
||
$tmpl->printPage();
|
||
|
||
exit();
|
||
}
|
||
}
|
||
$eventLogger->end('boot');
|
||
$eventLogger->log('init', 'OC::init', $loaderStart, microtime(true));
|
||
$eventLogger->start('runtime', 'Runtime');
|
||
$eventLogger->start('request', 'Full request after boot');
|
||
register_shutdown_function(function () use ($eventLogger) {
|
||
$eventLogger->end('request');
|
||
});
|
||
|
||
register_shutdown_function(function () use ($config) {
|
||
$memoryPeak = memory_get_peak_usage();
|
||
$debugModeEnabled = $config->getSystemValueBool('debug', false);
|
||
$memoryLimit = null;
|
||
|
||
if (!$debugModeEnabled) {
|
||
// Use the memory helper to get the real memory limit in bytes if debug mode is disabled
|
||
try {
|
||
$memoryInfo = new \OC\MemoryInfo();
|
||
$memoryLimit = $memoryInfo->getMemoryLimit();
|
||
} catch (Throwable $e) {
|
||
// Ignore any errors and fall back to hardcoded thresholds
|
||
}
|
||
}
|
||
|
||
// Check if a memory limit is configured and can be retrieved and determine log level if debug mode is disabled
|
||
if (!$debugModeEnabled && $memoryLimit !== null && $memoryLimit !== -1) {
|
||
$logLevel = match (true) {
|
||
$memoryPeak > $memoryLimit * 0.9 => ILogger::FATAL,
|
||
$memoryPeak > $memoryLimit * 0.75 => ILogger::ERROR,
|
||
$memoryPeak > $memoryLimit * 0.5 => ILogger::WARN,
|
||
default => null,
|
||
};
|
||
|
||
$memoryLimitIni = @ini_get('memory_limit');
|
||
$message = 'Request used ' . Util::humanFileSize($memoryPeak) . ' of memory. Memory limit: ' . ($memoryLimitIni ?: 'unknown');
|
||
} else {
|
||
// Fall back to hardcoded thresholds if memory_limit cannot be determined or if debug mode is enabled
|
||
$logLevel = match (true) {
|
||
$memoryPeak > 500_000_000 => ILogger::FATAL,
|
||
$memoryPeak > 400_000_000 => ILogger::ERROR,
|
||
$memoryPeak > 300_000_000 => ILogger::WARN,
|
||
default => null,
|
||
};
|
||
|
||
$message = 'Request used more than 300 MB of RAM: ' . Util::humanFileSize($memoryPeak);
|
||
}
|
||
|
||
// Log the message
|
||
if ($logLevel !== null) {
|
||
$logger = Server::get(LoggerInterface::class);
|
||
$logger->log($logLevel, $message, ['app' => 'core']);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* register hooks for the cleanup of cache and bruteforce protection
|
||
*/
|
||
public static function registerCleanupHooks(\OC\SystemConfig $systemConfig): void {
|
||
//don't try to do this before we are properly setup
|
||
if ($systemConfig->getValue('installed', false) && !\OCP\Util::needUpgrade()) {
|
||
// NOTE: This will be replaced to use OCP
|
||
$userSession = Server::get(\OC\User\Session::class);
|
||
$userSession->listen('\OC\User', 'postLogin', function () use ($userSession) {
|
||
if (!defined('PHPUNIT_RUN') && $userSession->isLoggedIn()) {
|
||
// reset brute force delay for this IP address and username
|
||
$uid = $userSession->getUser()->getUID();
|
||
$request = Server::get(IRequest::class);
|
||
$throttler = Server::get(IThrottler::class);
|
||
$throttler->resetDelay($request->getRemoteAddress(), 'login', ['user' => $uid]);
|
||
}
|
||
|
||
try {
|
||
$cache = new \OC\Cache\File();
|
||
$cache->gc();
|
||
} catch (\OC\ServerNotAvailableException $e) {
|
||
// not a GC exception, pass it on
|
||
throw $e;
|
||
} catch (\OC\ForbiddenException $e) {
|
||
// filesystem blocked for this request, ignore
|
||
} catch (\Exception $e) {
|
||
// a GC exception should not prevent users from using OC,
|
||
// so log the exception
|
||
Server::get(LoggerInterface::class)->warning('Exception when running cache gc.', [
|
||
'app' => 'core',
|
||
'exception' => $e,
|
||
]);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
private static function registerEncryptionWrapperAndHooks(): void {
|
||
/** @var \OC\Encryption\Manager */
|
||
$manager = Server::get(\OCP\Encryption\IManager::class);
|
||
Server::get(IEventDispatcher::class)->addListener(
|
||
BeforeFileSystemSetupEvent::class,
|
||
$manager->setupStorage(...),
|
||
);
|
||
|
||
$enabled = $manager->isEnabled();
|
||
if ($enabled) {
|
||
\OC\Encryption\EncryptionEventListener::register(Server::get(IEventDispatcher::class));
|
||
}
|
||
}
|
||
|
||
private static function registerAccountHooks(): void {
|
||
/** @var IEventDispatcher $dispatcher */
|
||
$dispatcher = Server::get(IEventDispatcher::class);
|
||
$dispatcher->addServiceListener(UserChangedEvent::class, \OC\Accounts\Hooks::class);
|
||
}
|
||
|
||
private static function registerAppRestrictionsHooks(): void {
|
||
/** @var \OC\Group\Manager $groupManager */
|
||
$groupManager = Server::get(\OCP\IGroupManager::class);
|
||
$groupManager->listen('\OC\Group', 'postDelete', function (\OCP\IGroup $group) {
|
||
$appManager = Server::get(\OCP\App\IAppManager::class);
|
||
$apps = $appManager->getEnabledAppsForGroup($group);
|
||
foreach ($apps as $appId) {
|
||
$restrictions = $appManager->getAppRestriction($appId);
|
||
if (empty($restrictions)) {
|
||
continue;
|
||
}
|
||
$key = array_search($group->getGID(), $restrictions, true);
|
||
unset($restrictions[$key]);
|
||
$restrictions = array_values($restrictions);
|
||
if (empty($restrictions)) {
|
||
$appManager->disableApp($appId);
|
||
} else {
|
||
$appManager->enableAppForGroups($appId, $restrictions);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
private static function registerResourceCollectionHooks(): void {
|
||
\OC\Collaboration\Resources\Listener::register(Server::get(IEventDispatcher::class));
|
||
}
|
||
|
||
private static function registerFileReferenceEventListener(): void {
|
||
\OC\Collaboration\Reference\File\FileReferenceEventListener::register(Server::get(IEventDispatcher::class));
|
||
}
|
||
|
||
private static function registerRenderReferenceEventListener() {
|
||
\OC\Collaboration\Reference\RenderReferenceEventListener::register(Server::get(IEventDispatcher::class));
|
||
}
|
||
|
||
/**
|
||
* register hooks for sharing
|
||
*/
|
||
public static function registerShareHooks(\OC\SystemConfig $systemConfig): void {
|
||
if ($systemConfig->getValue('installed')) {
|
||
|
||
$dispatcher = Server::get(IEventDispatcher::class);
|
||
$dispatcher->addServiceListener(UserRemovedEvent::class, UserRemovedListener::class);
|
||
$dispatcher->addServiceListener(GroupDeletedEvent::class, GroupDeletedListener::class);
|
||
$dispatcher->addServiceListener(UserDeletedEvent::class, UserDeletedListener::class);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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);
|
||
|
||
// Run setup if Nextcloud is not installed
|
||
if (!$installed) {
|
||
Server::get(ISession::class)->clear();
|
||
$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;
|
||
}
|
||
|
||
$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();
|
||
}
|
||
|
||
//
|
||
// At this point the request has passed the install/maintenance/upgrade gates
|
||
// or is using a path that is allowed to bypass them.
|
||
//
|
||
|
||
$appManager = Server::get(\OCP\App\IAppManager::class);
|
||
$userSession = Server::get(IUserSession::class);
|
||
$loggedIn = $userSession->isLoggedIn();
|
||
|
||
self::loadAuthenticationApps($appManager);
|
||
|
||
if ($loggedIn) {
|
||
self::loadAppsForAuthenticatedRequests($appManager);
|
||
} else {
|
||
self::loadAppsForPreAuthenticationPhase($appManager);
|
||
}
|
||
|
||
// 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 {
|
||
// 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;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
// Handle requests for JSON or XML
|
||
$acceptHeader = $request->getHeader('Accept');
|
||
if (in_array($acceptHeader, ['application/json', 'application/xml'], true)) {
|
||
http_response_code(404);
|
||
return;
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
if ($requestPath === '') {
|
||
// 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 {
|
||
// 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 {
|
||
$router->match('/error/404');
|
||
} catch (\Exception $e) {
|
||
if (!$e instanceof MethodNotAllowedException) {
|
||
logger('core')->emergency($e->getMessage(), ['exception' => $e]);
|
||
}
|
||
$l = Server::get(\OCP\L10N\IFactory::class)->get('lib');
|
||
Server::get(ITemplateManager::class)->printErrorPage(
|
||
'404',
|
||
$l->t('The page could not be found on the server.'),
|
||
404
|
||
);
|
||
}
|
||
}
|
||
|
||
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']);
|
||
}
|
||
|
||
/**
|
||
* 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')) {
|
||
return false;
|
||
}
|
||
$userSession = Server::get(\OC\User\Session::class);
|
||
if (OC_User::handleApacheAuth()) {
|
||
return true;
|
||
}
|
||
if (self::tryAppAPILogin($request)) {
|
||
return true;
|
||
}
|
||
if ($userSession->tryTokenLogin($request)) {
|
||
return true;
|
||
}
|
||
if (isset($_COOKIE['nc_username'])
|
||
&& isset($_COOKIE['nc_token'])
|
||
&& isset($_COOKIE['nc_session_id'])
|
||
&& $userSession->loginWithCookie($_COOKIE['nc_username'], $_COOKIE['nc_token'], $_COOKIE['nc_session_id'])) {
|
||
return true;
|
||
}
|
||
if ($userSession->tryBasicAuthLogin($request, Server::get(IThrottler::class))) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
protected static function handleAuthHeaders(): void {
|
||
//copy http auth headers for apache+php-fcgid work around
|
||
if (isset($_SERVER['HTTP_XAUTHORIZATION']) && !isset($_SERVER['HTTP_AUTHORIZATION'])) {
|
||
$_SERVER['HTTP_AUTHORIZATION'] = $_SERVER['HTTP_XAUTHORIZATION'];
|
||
}
|
||
|
||
// Extract PHP_AUTH_USER/PHP_AUTH_PW from other headers if necessary.
|
||
$vars = [
|
||
'HTTP_AUTHORIZATION', // apache+php-cgi work around
|
||
'REDIRECT_HTTP_AUTHORIZATION', // apache+php-cgi alternative
|
||
];
|
||
foreach ($vars as $var) {
|
||
if (isset($_SERVER[$var]) && is_string($_SERVER[$var]) && preg_match('/Basic\s+(.*)$/i', $_SERVER[$var], $matches)) {
|
||
$credentials = explode(':', base64_decode($matches[1]), 2);
|
||
if (count($credentials) === 2) {
|
||
$_SERVER['PHP_AUTH_USER'] = $credentials[0];
|
||
$_SERVER['PHP_AUTH_PW'] = $credentials[1];
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
protected static function tryAppAPILogin(OCP\IRequest $request): bool {
|
||
if (!$request->getHeader('AUTHORIZATION-APP-API')) {
|
||
return false;
|
||
}
|
||
$appManager = Server::get(OCP\App\IAppManager::class);
|
||
if (!$appManager->isEnabledForAnyone('app_api')) {
|
||
return false;
|
||
}
|
||
try {
|
||
$appAPIService = Server::get(OCA\AppAPI\Service\AppAPIService::class);
|
||
return $appAPIService->validateExAppRequestToNC($request);
|
||
} catch (\Psr\Container\NotFoundExceptionInterface|\Psr\Container\ContainerExceptionInterface $e) {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
OC::init();
|