Compare commits

...

4 Commits

Author SHA1 Message Date
Côme Chilliet b9c6206c2f chore: Small fixups and fixme comments
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-03-12 12:11:40 +01:00
Carl Schwan c7fca0071b fixup! refactor: Move base.php to a proper class 2026-03-12 09:30:55 +01:00
Carl Schwan dbc41af77f fixup! refactor: Move base.php to a proper class 2026-03-11 16:36:34 +01:00
Carl Schwan db558a7394 refactor: Move base.php to a proper class
Signed-off-by: Carl Schwan <carlschwan@kde.org>
2026-03-10 09:17:04 +01:00
23 changed files with 1839 additions and 1521 deletions
+2 -1
View File
@@ -6,6 +6,7 @@
*/
namespace OCA\Theming;
use OC\Kernel\Kernel;
use OCA\Theming\AppInfo\Application;
use OCA\Theming\Service\BackgroundService;
use OCP\App\AppPathNotFoundException;
@@ -395,7 +396,7 @@ class ThemingDefaults extends \OC_Defaults {
}
$route = $this->urlGenerator->linkToRoute('theming.Theming.getManifest', ['app' => $app ]);
}
if (str_starts_with($image, 'filetypes/') && file_exists(\OC::$SERVERROOT . '/core/img/' . $image)) {
if (str_starts_with($image, 'filetypes/') && file_exists(Kernel::getInstance()->getServerRoot() . '/core/img/' . $image)) {
$route = $this->urlGenerator->linkToRoute('theming.Icon.getThemedIcon', ['app' => $app, 'image' => $image]);
}
+8 -110
View File
@@ -2,120 +2,18 @@
declare(strict_types=1);
use OCP\IConfig;
use OCP\IURLGenerator;
use OCP\Server;
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
use OC\Kernel\ConsoleKernel;
require_once __DIR__ . '/lib/versioncheck.php';
require_once __DIR__ . '/lib/private/Kernel/Kernel.php';
require_once __DIR__ . '/lib/private/Kernel/ConsoleKernel.php';
use OC\Console\Application;
use OCP\AppFramework\Http\Response;
use OCP\Diagnostics\IEventLogger;
use OCP\IRequest;
use OCP\Profiler\IProfiler;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput;
define('OC_CONSOLE', 1);
function exceptionHandler($exception) {
echo 'An unhandled exception has been thrown:' . PHP_EOL;
echo $exception;
exit(1);
}
try {
require_once __DIR__ . '/lib/base.php';
// set to run indefinitely if needed
if (strpos(@ini_get('disable_functions'), 'set_time_limit') === false) {
@set_time_limit(0);
}
if (!OC::$CLI) {
echo 'This script can be run from the command line only' . PHP_EOL;
exit(1);
}
$config = Server::get(IConfig::class);
set_exception_handler('exceptionHandler');
if (!function_exists('posix_getuid')) {
echo 'The posix extensions are required - see https://www.php.net/manual/en/book.posix.php' . PHP_EOL;
exit(1);
}
$user = posix_getuid();
$configUser = fileowner(OC::$configDir . 'config.php');
if ($user !== $configUser) {
echo 'Console has to be executed with the user that owns the file config/config.php' . PHP_EOL;
echo 'Current user id: ' . $user . PHP_EOL;
echo 'Owner id of config.php: ' . $configUser . PHP_EOL;
echo "Try adding 'sudo -u #" . $configUser . "' to the beginning of the command (without the single quotes)" . PHP_EOL;
echo "If running with 'docker exec' try adding the option '-u " . $configUser . "' to the docker command (without the single quotes)" . PHP_EOL;
exit(1);
}
$oldWorkingDir = getcwd();
if ($oldWorkingDir === false) {
echo 'This script can be run from the Nextcloud root directory only.' . PHP_EOL;
echo "Can't determine current working dir - the script will continue to work but be aware of the above fact." . PHP_EOL;
} elseif ($oldWorkingDir !== __DIR__ && !chdir(__DIR__)) {
echo 'This script can be run from the Nextcloud root directory only.' . PHP_EOL;
echo "Can't change to Nextcloud root directory." . PHP_EOL;
exit(1);
}
if (!(function_exists('pcntl_signal') && function_exists('pcntl_signal_dispatch')) && !in_array('--no-warnings', $argv)) {
echo 'The process control (PCNTL) extensions are required in case you want to interrupt long running commands - see https://www.php.net/manual/en/book.pcntl.php' . PHP_EOL;
echo "Additionally the function 'pcntl_signal' and 'pcntl_signal_dispatch' need to be enabled in your php.ini." . PHP_EOL;
}
$eventLogger = Server::get(IEventLogger::class);
$eventLogger->start('console:build_application', 'Build Application instance and load commands');
$application = Server::get(Application::class);
/* base.php will have removed eventual debug options from argv in $_SERVER */
$argv = $_SERVER['argv'];
$input = new ArgvInput($argv);
$output = new ConsoleOutput();
$application->loadCommands($input, $output);
$eventLogger->end('console:build_application');
$eventLogger->start('console:run', 'Run the command');
$application->setAutoExit(false);
$exitCode = $application->run($input);
$eventLogger->end('console:run');
$profiler = Server::get(IProfiler::class);
if ($profiler->isEnabled()) {
$eventLogger->end('runtime');
$profile = $profiler->collect(Server::get(IRequest::class), new Response());
$profile->setMethod('occ');
$profile->setUrl(implode(' ', $argv));
$profiler->saveProfile($profile);
$urlGenerator = Server::get(IURLGenerator::class);
$url = $urlGenerator->linkToRouteAbsolute('profiler.main.profiler', [
'profiler' => 'db',
'token' => $profile->getToken(),
]);
$output->getErrorOutput()->writeln('Profiler output available at ' . $url);
}
if ($exitCode > 255) {
$exitCode = 255;
}
exit($exitCode);
} catch (Exception $ex) {
exceptionHandler($ex);
} catch (Error $ex) {
exceptionHandler($ex);
}
(new ConsoleKernel())
->boot()
->run($_SERVER['argv']);
+15 -13
View File
@@ -9,6 +9,10 @@ namespace OC\Core\Controller;
use OC\IntegrityCheck\Checker;
use OC\Setup;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IInitialStateService;
use OCP\IURLGenerator;
use OCP\Server;
@@ -31,7 +35,7 @@ class SetupController {
$this->autoConfigFile = \OC::$configDir . 'autoconfig.php';
}
public function run(array $post): void {
public function run(array $post): Response {
// Check for autosetup:
$post = $this->loadAutoConfig($post);
$opts = $this->setupHelper->getSystemInfo();
@@ -45,8 +49,7 @@ class SetupController {
}
if (!$this->setupHelper->canInstallFileExists()) {
$this->displaySetupForbidden();
return;
return $this->displaySetupForbidden();
}
if (isset($post['install']) && $post['install'] == 'true') {
@@ -56,21 +59,21 @@ class SetupController {
if (count($e) > 0) {
$options = array_merge($opts, $post, $errors);
$this->display($options);
return $this->display($options);
} else {
$this->finishSetup();
return $this->finishSetup();
}
} else {
$options = array_merge($opts, $post);
$this->display($options);
return $this->display($options);
}
}
private function displaySetupForbidden(): void {
$this->templateManager->printGuestPage('', 'installation_forbidden');
private function displaySetupForbidden(): TemplateResponse {
return new TemplateResponse('', 'installation_forbidden', [], TemplateResponse::RENDER_AS_GUEST);
}
public function display(array $post): void {
public function display(array $post): TemplateResponse {
$defaults = [
'adminlogin' => '',
'adminpass' => '',
@@ -103,10 +106,10 @@ class SetupController {
'adminDBConfiguration' => $this->urlGenerator->linkToDocs('admin-db-configuration'),
]);
$this->templateManager->printGuestPage('', 'installation');
return new TemplateResponse('', 'installation', [], TemplateResponse::RENDER_AS_GUEST);
}
private function finishSetup(): void {
private function finishSetup(): RedirectResponse {
if (file_exists($this->autoConfigFile)) {
unlink($this->autoConfigFile);
}
@@ -116,8 +119,7 @@ class SetupController {
$this->templateManager->printGuestPage('', 'installation_incomplete');
}
header('Location: ' . Server::get(IURLGenerator::class)->getAbsoluteURL('index.php/core/apps/recommended'));
exit();
return new RedirectResponse(Server::get(IURLGenerator::class)->getAbsoluteURL('index.php/core/apps/recommended'));
}
/**
+10 -92
View File
@@ -10,99 +10,17 @@ declare(strict_types=1);
require_once __DIR__ . '/lib/versioncheck.php';
use OC\ServiceUnavailableException;
use OC\User\LoginException;
use OCP\HintException;
use OC\Kernel\HttpKernel;
use OCP\AppFramework\Http\IOutput;
use OCP\IRequest;
use OCP\Security\Bruteforce\MaxDelayReached;
use OCP\Server;
use OCP\Template\ITemplateManager;
use Psr\Log\LoggerInterface;
try {
require_once __DIR__ . '/lib/base.php';
require_once __DIR__ . '/lib/private/Kernel/Kernel.php';
require_once __DIR__ . '/lib/private/Kernel/HttpKernel.php';
OC::handleRequest();
} catch (ServiceUnavailableException $ex) {
Server::get(LoggerInterface::class)->error($ex->getMessage(), [
'app' => 'index',
'exception' => $ex,
]);
$kernel = (new HttpKernel())
->boot();
//show the user a detailed error page
Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 503);
} catch (HintException $ex) {
try {
Server::get(ITemplateManager::class)->printErrorPage($ex->getMessage(), $ex->getHint(), 503);
} catch (Exception $ex2) {
try {
Server::get(LoggerInterface::class)->error($ex->getMessage(), [
'app' => 'index',
'exception' => $ex,
]);
Server::get(LoggerInterface::class)->error($ex2->getMessage(), [
'app' => 'index',
'exception' => $ex2,
]);
} catch (Throwable $e) {
// no way to log it properly - but to avoid a white page of death we try harder and ignore this one here
}
//show the user a detailed error page
Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500);
}
} catch (LoginException $ex) {
$request = Server::get(IRequest::class);
/**
* Routes with the @CORS annotation and other API endpoints should
* not return a webpage, so we only print the error page when html is accepted,
* otherwise we reply with a JSON array like the SecurityMiddleware would do.
*/
if (stripos($request->getHeader('Accept'), 'html') === false) {
http_response_code(401);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['message' => $ex->getMessage()]);
exit();
}
Server::get(ITemplateManager::class)->printErrorPage($ex->getMessage(), $ex->getMessage(), 401);
} catch (MaxDelayReached $ex) {
$request = Server::get(IRequest::class);
/**
* Routes with the @CORS annotation and other API endpoints should
* not return a webpage, so we only print the error page when html is accepted,
* otherwise we reply with a JSON array like the BruteForceMiddleware would do.
*/
if (stripos($request->getHeader('Accept'), 'html') === false) {
http_response_code(429);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['message' => $ex->getMessage()]);
exit();
}
http_response_code(429);
Server::get(ITemplateManager::class)->printGuestPage('core', '429');
} catch (Exception $ex) {
Server::get(LoggerInterface::class)->error($ex->getMessage(), [
'app' => 'index',
'exception' => $ex,
]);
//show the user a detailed error page
Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500);
} catch (Error $ex) {
try {
Server::get(LoggerInterface::class)->error($ex->getMessage(), [
'app' => 'index',
'exception' => $ex,
]);
} catch (Error $e) {
http_response_code(500);
header('Content-Type: text/plain; charset=utf-8');
print("Internal Server Error\n\n");
print("The server encountered an internal error and was unable to complete your request.\n");
print("Please contact the server administrator if this error reappears multiple times, please include the technical details below in your report.\n");
print("More details can be found in the webserver log.\n");
throw $ex;
}
Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500);
}
$request = $kernel->getServer()->get(IRequest::class);
$output = $kernel->getServer()->get(IOutput::class);
$response = $kernel->handle($request);
$kernel->deliverResponse($request, $response, $output);
+1 -1211
View File
File diff suppressed because it is too large Load Diff
+7 -5
View File
@@ -11,6 +11,7 @@ use OC\AppConfig;
use OC\AppFramework\Bootstrap\Coordinator;
use OC\Config\ConfigManager;
use OC\DB\MigrationService;
use OC\Kernel\Kernel;
use OC\Migration\BackgroundRepair;
use OCP\Activity\IManager as IActivityManager;
use OCP\App\AppPathNotFoundException;
@@ -33,6 +34,7 @@ use OCP\INavigationManager;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserSession;
use OCP\L10N\IFactory;
use OCP\Server;
use OCP\ServerVersion;
use OCP\Settings\IManager as ISettingsManager;
@@ -186,7 +188,7 @@ class AppManager implements IAppManager {
public function getAllAppsInAppsFolders(): array {
$apps = [];
foreach (\OC::$APPSROOTS as $apps_dir) {
foreach (Kernel::getInstance()->getAppsRoots() as $apps_dir) {
if (!is_readable($apps_dir['path'])) {
$this->logger->warning('unable to read app folder : ' . $apps_dir['path'], ['app' => 'core']);
continue;
@@ -725,7 +727,7 @@ class AppManager implements IAppManager {
*/
public function getAppWebPath(string $appId): string {
if (($dir = $this->findAppInDirectories($appId)) != false) {
return \OC::$WEBROOT . $dir['url'] . '/' . $appId;
return Kernel::getInstance()->getWebRoot() . $dir['url'] . '/' . $appId;
}
throw new AppPathNotFoundException('Could not find web path for ' . $appId);
}
@@ -751,7 +753,7 @@ class AppManager implements IAppManager {
}
$possibleApps = [];
foreach (\OC::$APPSROOTS as $dir) {
foreach (Kernel::getInstance()->getAppsRoots() as $dir) {
if (file_exists($dir['path'] . '/' . $appId)) {
$possibleApps[] = $dir;
}
@@ -928,7 +930,7 @@ class AppManager implements IAppManager {
*/
private function loadShippedJson(): void {
if ($this->shippedApps === null) {
$shippedJson = \OC::$SERVERROOT . '/core/shipped.json';
$shippedJson = Kernel::getInstance()->getServerRoot() . '/core/shipped.json';
if (!file_exists($shippedJson)) {
throw new \Exception("File not found: $shippedJson");
}
@@ -1052,7 +1054,7 @@ class AppManager implements IAppManager {
$appPath = $this->getAppPath($appId, true);
$this->clearAppsCache();
$l = \OC::$server->getL10N('core');
$l = Server::get(IFactory::class)->get('core');
$appData = $this->getAppInfo($appId, false, $l->getLanguageCode());
if ($appData === null) {
throw new AppPathNotFoundException('Could not find ' . $appId);
+10 -43
View File
@@ -15,13 +15,13 @@ use OC\Profiler\RoutingDataCollector;
use OCP\App\IAppManager;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\ICallbackResponse;
use OCP\AppFramework\Http\IOutput;
use OCP\AppFramework\QueryException;
use OCP\AppFramework\Http\Response;
use OCP\Diagnostics\IEventLogger;
use OCP\HintException;
use OCP\IRequest;
use OCP\Profiler\IProfiler;
use OCP\Server;
use Psr\Container\ContainerExceptionInterface;
/**
* Entry point for every request in your app. You can consider this as your
@@ -87,7 +87,7 @@ class App {
string $methodName,
DIContainer $container,
?array $urlParams = null,
): void {
): Response {
/** @var IProfiler $profiler */
$profiler = $container->get(IProfiler::class);
$eventLogger = $container->get(IEventLogger::class);
@@ -118,7 +118,7 @@ class App {
// first try $controllerName then go for \OCA\AppName\Controller\$controllerName
try {
$controller = $container->get($controllerName);
} catch (QueryException $e) {
} catch (ContainerExceptionInterface $e) {
if (str_contains($controllerName, '\\Controller\\')) {
// This is from a global registered app route that is not enabled.
[/*OC(A)*/, $app, /* Controller/Name*/] = explode('\\', $controllerName, 3);
@@ -131,7 +131,7 @@ class App {
$appNameSpace = self::buildAppNamespace($appName);
}
$controllerName = $appNameSpace . '\\Controller\\' . $controllerName;
$controller = $container->query($controllerName);
$controller = $container->get($controllerName);
}
$eventLogger->end('app:controller:load');
@@ -145,53 +145,20 @@ class App {
$eventLogger->start('app:controller:run', 'Run app controller');
[
$httpHeaders,
$responseHeaders,
$responseCookies,
$output,
$response
] = $dispatcher->dispatch($controller, $methodName);
$response = $dispatcher->dispatch($controller, $methodName);
$eventLogger->end('app:controller:run');
$io = $container[IOutput::class];
if ($profiler->isEnabled()) {
$eventLogger->end('runtime');
$profile = $profiler->collect($container->get(IRequest::class), $response);
$profiler->saveProfile($profile);
$io->setHeader('X-Debug-Token:' . $profile->getToken());
$io->setHeader('Server-Timing: token;desc="' . $profile->getToken() . '"');
}
if (!is_null($httpHeaders)) {
$io->setHeader($httpHeaders);
}
foreach ($responseHeaders as $name => $value) {
$io->setHeader($name . ': ' . $value);
}
foreach ($responseCookies as $name => $value) {
$expireDate = null;
if ($value['expireDate'] instanceof \DateTime) {
$expireDate = $value['expireDate']->getTimestamp();
}
$sameSite = $value['sameSite'] ?? 'Lax';
$io->setCookie(
$name,
$value['value'],
$expireDate,
$container->getServer()->getWebRoot(),
null,
$container->getServer()->get(IRequest::class)->getServerProtocol() === 'https',
true,
$sameSite
);
$response->addHeader('X-Debug-Token', $profile->getToken());
$response->addHeader('Server-Timing', 'token;desc="' . $profile->getToken() . '"');
}
return $response;
//FIXME dead code below, should be migrated
/*
* Status 204 does not have a body and no Content Length
* Status 304 does not have a body and does not need a Content Length
+3 -7
View File
@@ -15,12 +15,10 @@ class Http extends BaseHttp {
protected $headers;
/**
* @param array $server $_SERVER
* @param string $protocolVersion the http version to use defaults to HTTP/1.1
*/
public function __construct(
private $server,
private $protocolVersion = 'HTTP/1.1',
private string $protocolVersion = 'HTTP/1.1',
) {
$this->headers = [
self::STATUS_CONTINUE => 'Continue',
@@ -88,12 +86,10 @@ class Http extends BaseHttp {
/**
* Gets the correct header
* @param int Http::CONSTANT $status the constant from the Http class
* @param \DateTime $lastModified formatted last modified date
* @param string $ETag the etag
* @param Http::STATUS_* $status the constant from the Http class
* @return string
*/
public function getStatusHeader($status) {
public function getStatusHeader(int $status): string {
// we have one change currently for the http 1.0 header that differs
// from 1.1: STATUS_TEMPORARY_REDIRECT should be STATUS_FOUND
// if this differs any more, we want to create childclasses for this
+7 -18
View File
@@ -32,7 +32,6 @@ class Dispatcher {
* runs the middleware
*/
public function __construct(
private readonly Http $protocol,
private readonly MiddlewareDispatcher $middlewareDispatcher,
private readonly ControllerMethodReflector $reflector,
private readonly IRequest $request,
@@ -50,15 +49,9 @@ class Dispatcher {
* @param Controller $controller the controller which will be called
* @param string $methodName the method name which will be called on
* the controller
* @return array{0: string, 1: array, 2: array, 3: string, 4: Response}
* $array[0] contains the http status header as a string,
* $array[1] contains response headers as an array,
* $array[2] contains response cookies as an array,
* $array[3] contains the response output as a string,
* $array[4] contains the response object
* @throws \Exception
*/
public function dispatch(Controller $controller, string $methodName): array {
public function dispatch(Controller $controller, string $methodName): Response {
try {
// prefill reflector with everything that's needed for the
// middlewares
@@ -112,16 +105,12 @@ class Dispatcher {
$response = $this->middlewareDispatcher->afterController(
$controller, $methodName, $response);
// depending on the cache object the headers need to be changed
return [
$this->protocol->getStatusHeader($response->getStatus()),
array_merge($response->getHeaders()),
$response->getCookies(),
$this->middlewareDispatcher->beforeOutput(
$controller, $methodName, $response->render()
),
$response,
];
// TODO
$this->middlewareDispatcher->beforeOutput(
$controller, $methodName, $response->render()
);
return $response;
}
+2 -1
View File
@@ -13,6 +13,7 @@ use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Event\Listeners\OracleSessionInit;
use OC\DB\QueryBuilder\Sharded\AutoIncrementHandler;
use OC\DB\QueryBuilder\Sharded\ShardConnectionManager;
use OC\Kernel\Kernel;
use OC\SystemConfig;
use OCP\ICacheFactory;
use OCP\Server;
@@ -186,7 +187,7 @@ class ConnectionFactory {
$name = $this->config->getValue($configPrefix . 'dbname', $this->config->getValue('dbname', self::DEFAULT_DBNAME));
if ($this->normalizeType($type) === 'sqlite3') {
$dataDir = $this->config->getValue('datadirectory', \OC::$SERVERROOT . '/data');
$dataDir = $this->config->getValue('datadirectory', Kernel::getInstance()->getServerRoot() . '/data');
$connectionParams['path'] = $dataDir . '/' . $name . '.db';
} else {
$host = $this->config->getValue($configPrefix . 'dbhost', $this->config->getValue('dbhost', ''));
+143
View File
@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH
* SPDX-FileContributor: Carl Schwan
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Kernel;
use OC\Console\Application;
use OC\SystemConfig;
use OCP\App\IAppManager;
use OCP\AppFramework\Http\Response;
use OCP\Diagnostics\IEventLogger;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\Profiler\IProfiler;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput;
use Throwable;
class ConsoleKernel extends Kernel {
public function __construct() {
parent::__construct();
set_exception_handler($this->exceptionHandler(...));
// set to run indefinitely if needed
if (strpos(@ini_get('disable_functions'), 'set_time_limit') === false) {
@set_time_limit(0);
}
if (!$this->isCli()) {
echo 'This script can be run from the command line only' . PHP_EOL;
exit(1);
}
if (!function_exists('posix_getuid')) {
echo 'The posix extensions are required - see https://www.php.net/manual/en/book.posix.php' . PHP_EOL;
exit(1);
}
}
public function boot(): self {
try {
parent::boot();
$user = posix_getuid();
$configUser = fileowner($this->configDir . 'config.php');
if ($user !== $configUser) {
echo 'Console has to be executed with the user that owns the file config/config.php' . PHP_EOL;
echo 'Current user id: ' . $user . PHP_EOL;
echo 'Owner id of config.php: ' . $configUser . PHP_EOL;
echo "Try adding 'sudo -u #" . $configUser . "' to the beginning of the command (without the single quotes)" . PHP_EOL;
echo "If running with 'docker exec' try adding the option '-u " . $configUser . "' to the docker command (without the single quotes)" . PHP_EOL;
exit(1);
}
$oldWorkingDir = getcwd();
if ($oldWorkingDir === false) {
echo 'This script can be run from the Nextcloud root directory only.' . PHP_EOL;
echo "Can't determine current working dir - the script will continue to work but be aware of the above fact." . PHP_EOL;
} elseif ($oldWorkingDir !== __DIR__ && !chdir(__DIR__)) {
echo 'This script can be run from the Nextcloud root directory only.' . PHP_EOL;
echo "Can't change to Nextcloud root directory." . PHP_EOL;
exit(1);
}
return $this;
} catch (Throwable $e) {
echo $e->getMessage() . PHP_EOL . $e->getTraceAsString() . PHP_EOL;
exit(1);
}
}
public function run(array $argv): void {
try {
if (!(function_exists('pcntl_signal') && function_exists('pcntl_signal_dispatch')) && !in_array('--no-warnings', $argv)) {
echo 'The process control (PCNTL) extensions are required in case you want to interrupt long running commands - see https://www.php.net/manual/en/book.pcntl.php' . PHP_EOL;
echo "Additionally the function 'pcntl_signal' and 'pcntl_signal_dispatch' need to be enabled in your php.ini." . PHP_EOL;
}
$eventLogger = $this->server->get(IEventLogger::class);
$eventLogger->start('console:build_application', 'Build Application instance and load commands');
$application = $this->server->get(Application::class);
/* base.php will have removed eventual debug options from argv in $_SERVER */
$input = new ArgvInput($argv);
$output = new ConsoleOutput();
$application->loadCommands($input, $output);
$eventLogger->end('console:build_application');
$eventLogger->start('console:run', 'Run the command');
$application->setAutoExit(false);
$exitCode = $application->run($input);
$eventLogger->end('console:run');
$profiler = $this->server->get(IProfiler::class);
if ($profiler->isEnabled()) {
$eventLogger->end('runtime');
$profile = $profiler->collect($this->server->get(IRequest::class), new Response());
$profile->setMethod('occ');
$profile->setUrl(implode(' ', $argv));
$profiler->saveProfile($profile);
if ($this->server->get(IAppManager::class)->isEnabledForAnyone('profiler')) {
$urlGenerator = $this->server->get(IURLGenerator::class);
$url = $urlGenerator->linkToRouteAbsolute('profiler.main.profiler', [
'profiler' => 'db',
'token' => $profile->getToken(),
]);
$output->getErrorOutput()->writeln('Profiler output available at ' . $url);
}
}
if ($exitCode > 255) {
$exitCode = 255;
}
exit($exitCode);
} catch (Throwable $e) {
echo $e->getMessage() . PHP_EOL . $e->getTraceAsString() . PHP_EOL;
exit(1);
}
}
function exceptionHandler($exception): never {
echo 'An unhandled exception has been thrown:' . PHP_EOL;
echo $exception;
exit(1);
}
protected function setupSession(IRequest $request, IEventLogger $eventLogger): void {
}
public function checkInstalled(SystemConfig $systemConfig): void {
}
}
+677
View File
@@ -0,0 +1,677 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Kernel;
use Error;
use Exception;
use OC\AppFramework\Http;
use OC\Core\Controller\SetupController;
use OC\ServiceUnavailableException;
use OC\SystemConfig;
use OC\User\DisabledUserException;
use OC\User\LoginException;
use OC_User;
use OC_Util;
use OCA\AppAPI\Service\AppAPIService;
use OCP\App\IAppManager;
use OCP\AppFramework\Http\ErrorTemplateResponse;
use OCP\AppFramework\Http\IOutput;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\Diagnostics\IEventLogger;
use OCP\HintException;
use OCP\IAppConfig;
use OCP\IConfig;
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUserSession;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Security\Bruteforce\MaxDelayReached;
use OCP\Server;
use OCP\Template\ITemplateManager;
use OCP\Util;
use Psr\Log\LoggerInterface;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Throwable;
use function OCP\Log\logger;
class HttpKernel extends Kernel {
public function handle(IRequest $request): Response {
try {
return $this->doHandle($request);
} catch (ServiceUnavailableException $ex) {
Server::get(LoggerInterface::class)->error($ex->getMessage(), [
'app' => 'index',
'exception' => $ex,
]);
//show the user a detailed error page
Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 503);
} catch (HintException $ex) {
try {
Server::get(ITemplateManager::class)->printErrorPage($ex->getMessage(), $ex->getHint(), 503);
} catch (Exception $ex2) {
try {
Server::get(LoggerInterface::class)->error($ex->getMessage(), [
'app' => 'index',
'exception' => $ex,
]);
Server::get(LoggerInterface::class)->error($ex2->getMessage(), [
'app' => 'index',
'exception' => $ex2,
]);
} catch (Throwable $e) {
// no way to log it properly - but to avoid a white page of death we try harder and ignore this one here
}
//show the user a detailed error page
Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500);
}
} catch (LoginException $ex) {
$request = Server::get(IRequest::class);
/**
* Routes with the @CORS annotation and other API endpoints should
* not return a webpage, so we only print the error page when html is accepted,
* otherwise we reply with a JSON array like the SecurityMiddleware would do.
*/
if (stripos($request->getHeader('Accept'), 'html') === false) {
http_response_code(401);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['message' => $ex->getMessage()]);
exit();
}
Server::get(ITemplateManager::class)->printErrorPage($ex->getMessage(), $ex->getMessage(), 401);
} catch (MaxDelayReached $ex) {
$request = Server::get(IRequest::class);
/**
* Routes with the @CORS annotation and other API endpoints should
* not return a webpage, so we only print the error page when html is accepted,
* otherwise we reply with a JSON array like the BruteForceMiddleware would do.
*/
if (stripos($request->getHeader('Accept'), 'html') === false) {
http_response_code(429);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['message' => $ex->getMessage()]);
exit();
}
http_response_code(429);
Server::get(ITemplateManager::class)->printGuestPage('core', '429');
} catch (Exception $ex) {
Server::get(LoggerInterface::class)->error($ex->getMessage(), [
'app' => 'index',
'exception' => $ex,
]);
//show the user a detailed error page
Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500);
} catch (Error $ex) {
try {
Server::get(LoggerInterface::class)->error($ex->getMessage(), [
'app' => 'index',
'exception' => $ex,
]);
} catch (Error $e) {
http_response_code(500);
header('Content-Type: text/plain; charset=utf-8');
print("Internal Server Error\n\n");
print("The server encountered an internal error and was unable to complete your request.\n");
print("Please contact the server administrator if this error reappears multiple times, please include the technical details below in your report.\n");
print("More details can be found in the webserver log.\n");
throw $ex;
}
Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500);
}
}
public function doHandle(IRequest $request): Response {
$this->getContainer()->get(IEventLogger::class)
->start('handle_request', 'Handle request');
// Check if Nextcloud is installed or in maintenance (update) mode
if (!$this->getSystemConfig()->getValue('installed', false)) {
$this->getContainer()->get(ISession::class)->clear();
$controller = $this->getContainer()->get(SetupController::class);
return $controller->run($_POST);
}
$request->throwDecodingExceptionIfAny();
$requestPath = $request->getRawPathInfo();
if (substr($requestPath, -3) !== '.js') { // we need these files during the upgrade
$this->checkMaintenanceMode($this->getSystemConfig());
if (Util::needUpgrade()) {
if (function_exists('opcache_reset')) {
opcache_reset();
}
if (!((bool)$this->getSystemConfig()->getValue('maintenance', false))) {
/** @var UpgradePageController $upgradePageController */
$upgradePageController = $this->getContainer()->get(UpgradePageController::class);
return $upgradePageController->printUpgradePage($this->getSystemConfig());
}
}
}
$appManager = $this->getContainer()->get(\OCP\App\IAppManager::class);
// Always load authentication apps
$appManager->loadApps(['authentication']);
$appManager->loadApps(['extended_authentication']);
// Load minimum set of apps
if (!Util::needUpgrade() && !((bool)($this->getSystemConfig()->getValue('maintenance', false)))) {
// For logged-in users: Load everything
if ($this->getContainer()->get(IUserSession::class)->isLoggedIn()) {
$appManager->loadApps();
} else {
// For guests: Load only filesystem and logging
$appManager->loadApps(['filesystem', 'logging']);
// Don't try to log in 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;
}
}
}
}
}
try {
if (!Util::needUpgrade()) {
$appManager->loadApps(['filesystem', 'logging']);
$appManager->loadApps();
}
return Server::get(\OC\Route\Router::class)->match($request);
} catch (Symfony\Component\Routing\Exception\ResourceNotFoundException $e) {
//header('HTTP/1.0 404 Not Found');
} catch (Symfony\Component\Routing\Exception\MethodNotAllowedException $e) {
return new ErrorTemplateResponse('', '', status: 405);
}
// Handle WebDAV
if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PROPFIND') {
// not allowed anymore to prevent people
// mounting this root directly.
// Users need to mount remote.php/webdav instead.
return new ErrorTemplateResponse('', '', status: 405);
}
// Handle requests for JSON or XML
$acceptHeader = $request->getHeader('Accept');
if (in_array($acceptHeader, ['application/json', 'application/xml'], true)) {
return new NotFoundResponse();
}
// 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.
$destinationHeader = $request->getHeader('Sec-Fetch-Dest');
if (in_array($destinationHeader, ['font', 'script', 'style'])) {
return new NotFoundResponse();
}
// Redirect to the default app or login only as an entry point
if ($requestPath === '') {
// Someone is logged in
$userSession = Server::get(IUserSession::class);
if ($userSession->isLoggedIn()) {
$response = new RedirectResponse(Server::get(IURLGenerator::class)->linkToDefaultPageUrl());
$response->addHeader('X-User-Id', $userSession->getUser()?->getUID());
return $response;
} else {
// Not handled and not logged in
return new RedirectResponse(Server::get(IURLGenerator::class)->linkToRouteAbsolute('core.login.showLoginForm'));
}
}
try {
return Server::get(\OC\Route\Router::class)->match('/error/404');
} catch (\Exception $e) {
if (!$e instanceof MethodNotAllowedException) {
logger('core')->emergency($e->getMessage(), ['exception' => $e]);
}
return new NotFoundResponse();
}
}
public function checkMaintenanceMode(SystemConfig $systemConfig): ?TemplateResponse {
// Allow ajax update script to execute without being stopped
if (!((bool)$systemConfig->getValue('maintenance', false)) || $this->getSubUri() === '/core/ajax/update.php') {
return null;
}
$response = new TemplateResponse('', 'update.user', [], TemplateResponse::RENDER_AS_GUEST, 503);
$response->setHeaders('X-Nextcloud-Maintenance-Mode', '1');
$response->setHeaders('Retry-After', '120');
Util::addScript('core', 'maintenance');
Util::addScript('core', 'common');
Util::addStyle('core', 'guest');
return $response;
}
public function boot(): self {
$this->handleAuthHeaders();
parent::boot();
$this->eventLogger->start('check_server', 'Run a few configuration checks');
$errors = OC_Util::checkServer($this->systemConfig);
if (count($errors) > 0) {
if (!$this->isCli()) {
http_response_code(503);
Util::addStyle('guest');
try {
$this->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 {
$this->server->get(IAppConfig::class)->setValueArray('core', 'cronErrors', $staticErrors);
} catch (\Exception $e) {
echo('Writing to database failed');
}
exit(1);
} elseif ($this->isCli() && $this->systemConfig->getValue('installed', false)) {
$this->server->get(IAppConfig::class)->deleteKey('core', 'cronErrors');
}
$this->eventLogger->end('check_server');
return $this;
}
protected function setupSession(IRequest $request, IEventLogger $eventLogger): void {
$eventLogger->start('init_session', 'Initialize session');
$systemConfig = $this->server->get(SystemConfig::class);
$appManager = $this->server->get(\OCP\App\IAppManager::class);
if ($systemConfig->getValue('installed', false)) {
$appManager->loadApps(['session']);
}
$this->initSession($request);
$eventLogger->end('init_session');
$this->checkInstalled($systemConfig);
$this->addSecurityHeaders();
$this->performSameSiteCookieProtection($request, $this->server->get(IConfig::class));
}
public function initSession(IRequest $request): void {
// 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 = $this->webRoot ? : '/';
ini_set('session.cookie_path', $cookie_path);
// set the cookie domain to the Nextcloud domain
$cookie_domain = $this->getSystemConfig()->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(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);
$this->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, $this->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);
}
private 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;
}
}
}
}
/**
* Craft the response output based on the given Response object.
*/
public function deliverResponse(IRequest $request, Response $response, IOutput $io): void {
/** @var Http $protocol */
$protocol = $this->getContainer()->get(Http::class);
$protocol->getStatusHeader($response->getStatus());
// Headers
foreach ($response->getHeaders() as $name => $value) {
$io->setHeader($name . ': ' . $value);
}
// Output
//FIXME middleware are skipped
$io->setOutput($response->render());
// Cookies
foreach ($response->getCookies() as $name => $value) {
$expireDate = null;
if ($value['expireDate'] instanceof \DateTime) {
$expireDate = $value['expireDate']->getTimestamp();
}
$sameSite = $value['sameSite'] ?? 'Lax';
$io->setCookie(
$name,
$value['value'],
$expireDate,
$this->getWebRoot(),
null,
$request->getServerProtocol() === 'https',
true,
$sameSite
);
}
}
/**
* Check login: apache auth, auth token, basic auth
*/
public function handleLogin(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 ($this->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 function tryAppAPILogin(IRequest $request): bool {
if (!$request->getHeader('AUTHORIZATION-APP-API')) {
return false;
}
$appManager = Server::get(IAppManager::class);
if (!$appManager->isEnabledForAnyone('app_api')) {
return false;
}
try {
$appAPIService = Server::get(AppAPIService::class);
return $appAPIService->validateExAppRequestToNC($request);
} catch (\Psr\Container\NotFoundExceptionInterface|\Psr\Container\ContainerExceptionInterface $e) {
return false;
}
}
public function checkInstalled(\OC\SystemConfig $systemConfig): void {
// Redirect to installer if not installed
if (!$systemConfig->getValue('installed', false) && $this->subUri !== '/index.php' && $this->subUri !== '/status.php') {
if ($this->isCli()) {
throw new Exception('Not installed');
} else {
$url = $this->webRoot . '/index.php';
header('Location: ' . $url);
}
exit();
}
}
/**
* Send the same site cookies
*/
private 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 function performSameSiteCookieProtection(IRequest $request, IConfig $config): void {
// 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 these 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-' . \OC::$server->getContentSecurityPolicyNonceManager()->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
}
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Kernel;
use OCP\AppFramework\Http\Response;
use OCP\IRequest;
interface IHttpKernel {
/**
* Handle the request
*/
public function handle(IRequest $request, bool $catch = true): Response;
}
+24
View File
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Kernel;
use OCP\AppFramework\Http\Response;
use OCP\IRequest;
use Psr\Container\ContainerInterface;
interface IKernel {
public function boot(): void;
public function getServerRoot(): string;
public function isCli(): bool;
public function getContainer(): ContainerInterface;
}
+671
View File
@@ -0,0 +1,671 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Kernel;
use Composer\Autoload\ClassLoader;
use OC\AllConfig;
use OC\AppFramework\Bootstrap\Coordinator;
use OC\AppFramework\Http\Request;
use OC\AppFramework\Http\RequestId;
use OC\AppFramework\Utility\SimpleContainer;
use OC\Config;
use OC\Core\Listener\BeforeMessageLoggedEventListener;
use OC\Log\ErrorHandler;
use OC\Profiler\BuiltInProfiler;
use OC\Security\SecureRandom;
use OC\Server;
use OC\Share20\GroupDeletedListener;
use OC\Share20\UserDeletedListener;
use OC\Share20\UserRemovedListener;
use OC\SystemConfig;
use OC_User;
use OC_Util;
use OCP\App\IAppManager;
use OCP\Console\ReservedOptions;
use OCP\Diagnostics\IEventLogger;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Group\Events\GroupDeletedEvent;
use OCP\Group\Events\UserRemovedEvent;
use OCP\HintException;
use OCP\IConfig;
use OCP\ILogger;
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Template\ITemplateManager;
use OCP\User\Events\UserChangedEvent;
use OCP\User\Events\UserDeletedEvent;
use OCP\Util;
use Override;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Throwable;
use function OCP\Log\logger;
require_once __DIR__ . '/../../base.php';
abstract class Kernel {
protected ClassLoader $composerAutoloader;
protected Config $config;
protected SystemConfig $systemConfig;
protected IEventLogger $eventLogger;
protected string $serverRoot;
protected string $webRoot;
protected string $configDir;
protected string $subUri;
protected array $appsRoots = [];
public Server $server;
static Kernel $kernel;
public function __construct(
) {
$this->serverRoot = str_replace('\\', '/', substr(__DIR__, 0, -18));
\OC::$SERVERROOT = $this->serverRoot;
if (isset(self::$kernel)) {
throw new RuntimeException("Kernel is already initialized");
}
self::$kernel = $this;
}
public static function getInstance(): self {
return self::$kernel;
}
public function getServer(): Server {
return $this->server;
}
public function boot(): self {
$this->setRequiredIniValues();
$this->setupPhpDefault();
[$loaderStart, $loaderEnd] = $this->setupAutoloader();
$this->setupServerContainer();
$this->eventLogger = $this->server->get(IEventLogger::class);
$this->eventLogger->log('autoloader', 'Autoloader', $loaderStart, $loaderEnd);
$this->eventLogger->start('boot', 'Initialize');
$this->setupLogging();
// initialize intl fallback if necessary
OC_Util::isSetLocaleWorking();
$this->setupErrorHandler();
/** @var Coordinator $bootstrapCoordinator */
$bootstrapCoordinator = $this->server->get(Coordinator::class);
$bootstrapCoordinator->runInitialRegistration();
$this->setupSession($this->server->get(IRequest::class), $this->eventLogger);
if (!$this->checkConfig()) {
// TODO
}
$this->checkInstalled($this->systemConfig);
// User and Groups
if (!$this->systemConfig->getValue('installed', false)) {
$this->server->get(ISession::class)->set('user_id', '');
}
$this->eventLogger->start('setup_backends', 'Setup group and user backends');
$this->server->get(\OCP\IUserManager::class)->registerBackend(new \OC\User\Database());
$this->server->get(\OCP\IGroupManager::class)->addBackend(new \OC\Group\Database());
// Subscribe to the hook
Util::connectHook(
'\OCA\Files_Sharing\API\Server2Server',
'preLoginNameUsedAsUserName',
'\OC\User\Database',
'preLoginNameUsedAsUserName'
);
//setup extra user backends
if (!Util::needUpgrade()) {
OC_User::setupBackends();
} else {
// Run upgrades in incognito mode
OC_User::setIncognitoMode(true);
}
$this->eventLogger->end('setup_backends');
self::registerCleanupHooks($this->systemConfig);
self::registerShareHooks($this->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 set up
if ($this->systemConfig->getValue('installed', false)) {
$appManager = $this->server->get(IAppManager::class);
$appManager->loadApp('settings');
}
//make sure temporary files are cleaned up
$tmpManager = $this->server->get(\OCP\ITempManager::class);
register_shutdown_function([$tmpManager, 'clean']);
$lockProvider = $this->server->get(\OCP\Lock\ILockingProvider::class);
register_shutdown_function([$lockProvider, 'releaseAll']);
// Check whether the sample configuration has been copied
if ($this->systemConfig->getValue('copied_sample_config', false)) {
$l = $this->server->get(\OCP\L10N\IFactory::class)->get('lib');
$this->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 $this;
}
$request = $this->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 (!$this->isCli()
&& !$this->server->get(\OC\Security\TrustedDomainHelper::class)->isTrustedDomain($host)
&& $this->systemConfig->getValue('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);
$this->server->get(LoggerInterface::class)->info(
'Trusted domain error. "{remoteAddress}" tried to access using "{host}" as host.',
[
'app' => 'core',
'remoteAddress' => $request->getRemoteAddress(),
'host' => $host,
]
);
$tmpl = $this->server->get(ITemplateManager::class)->getTemplate('core', 'untrustedDomain', 'guest');
$tmpl->assign('docUrl', $this->server->get(IURLGenerator::class)->linkToDocs('admin-trusted-domains'));
$tmpl->printPage();
exit();
}
}
$this->eventLogger->end('boot');
$this->eventLogger->log('init', 'OC::init', $loaderStart, microtime(true));
$this->eventLogger->start('runtime', 'Runtime');
$this->eventLogger->start('request', 'Full request after boot');
register_shutdown_function(function () {
$this->eventLogger->end('request');
});
register_shutdown_function(function () {
$memoryPeak = memory_get_peak_usage();
$debugModeEnabled = $this->systemConfig->getValue('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 = $this->server->get(LoggerInterface::class);
$logger->log($logLevel, $message, ['app' => 'core']);
}
});
return $this;
}
public function isCli(): bool {
return php_sapi_name() === 'cli';
}
public function getServerRoot(): string {
return $this->serverRoot;
}
/**
* Try to set some values to the required Nextcloud default
*/
private 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');
}
/**
* @throws \RuntimeException when the 3rdparty directory is missing or
* the app path list is empty or contains an invalid path
*/
public function initPaths(): void {
if (defined('PHPUNIT_CONFIG_DIR')) {
$this->configDir = $this->getServerRoot() . '/' . PHPUNIT_CONFIG_DIR . '/';
} elseif (defined('PHPUNIT_RUN') && PHPUNIT_RUN && is_dir($this->getServerRoot() . '/tests/config/')) {
$this->configDir = $this->getServerRoot() . '/tests/config/';
} elseif ($dir = getenv('NEXTCLOUD_CONFIG_DIR')) {
$this->configDir = rtrim($dir, '/') . '/';
} else {
$this->configDir = $this->getServerRoot() . '/config/';
}
\OC::$configDir = $this->configDir;
$this->config = new Config($this->configDir);
$this->systemConfig = new SystemConfig($this->config);
$this->subUri = str_replace('\\', '/', substr(realpath($_SERVER['SCRIPT_FILENAME'] ?? ''), strlen($this->getServerRoot())));
/**
* FIXME: The following lines are required because we can't yet instantiate
* $this->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 Request(
$params,
new RequestId($_SERVER['UNIQUE_ID'] ?? '', new SecureRandom()),
new AllConfig($this->systemConfig)
);
$scriptName = $fakeRequest->getScriptName();
if (substr($scriptName, -1) == '/') {
$scriptName .= 'index.php';
//make sure suburi follows the same rules as scriptName
if (substr($this->subUri, -9) != 'index.php') {
if (substr($this->subUri, -1) != '/') {
$this->subUri = $this->subUri . '/';
}
$this->subUri = $this->subUri . 'index.php';
}
}
if ($this->isCli()) {
$this->webRoot = $this->config->getValue('overwritewebroot', '');
} else {
if (substr($scriptName, 0 - strlen($this->subUri)) === $this->subUri) {
$this->webRoot = substr($scriptName, 0, 0 - strlen($this->subUri));
if ($this->webRoot != '' && $this->webRoot[0] !== '/') {
$this->webRoot = '/' . $this->webRoot;
}
} else {
// The scriptName is not ending with Kernel::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.
$this->webRoot = $this->config->getValue('overwritewebroot', '');
}
\OC::$WEBROOT = $this->webRoot;
// 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'] === $this->webRoot
&& substr($_SERVER['REQUEST_URI'], -1) !== '/') {
header('Location: ' . $this->webRoot . '/');
exit();
}
}
// search the apps folder
$config_paths = $this->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'], '/');
$this->appsRoots[] = $paths;
}
}
} elseif (file_exists($this->serverRoot . '/apps')) {
$this->appsRoots[] = ['path' => $this->serverRoot . '/apps', 'url' => '/apps', 'writable' => true];
}
if ($this->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.');
}
\OC::$APPSROOTS = $this->appsRoots;
$paths = [];
foreach ($this->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 function getConfigDir(): string {
return $this->configDir;
}
public function getAppsRoots(): array {
return $this->appsRoots;
}
public function addPsr4(string $prefix, string $path, bool $prepend = false): void {
$this->composerAutoloader->addPsr4($prefix, $path, $prepend);
}
public function getWebRoot(): string {
return $this->webRoot;
}
public function getContainer(): ContainerInterface {
return $this->server;
}
protected function getSystemConfig(): SystemConfig {
return $this->systemConfig;
}
protected function getSubUri(): string {
return $this->subUri;
}
public function checkConfig(): bool {
// Create config if it does not already exist
$configFilePath = $this->configDir . '/config.php';
if (!file_exists($configFilePath)) {
@touch($configFilePath);
}
// Check if config is writable
$configFileWritable = is_writable($configFilePath);
$configReadOnly = $this->config->getValue('config_is_read_only', false);
return !(!$configFileWritable && !$configReadOnly || !$configFileWritable && Util::needUpgrade());
}
/**
* @return array{float, float}
*/
protected function setupAutoloader(): array {
// register autoloader
$loaderStart = microtime(true);
// Add default composer PSR-4 autoloader, ensure apcu to be disabled
$this->composerAutoloader = require_once $this->getServerRoot() . '/lib/composer/autoload.php';
$this->composerAutoloader->setApcuPrefix(null);
try {
$this->initPaths();
// setup 3rdparty autoloader
$vendorAutoLoad = $this->getServerRoot() . '/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 (!$this->isCli()) {
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);
return [$loaderStart, $loaderEnd];
}
protected function setupErrorHandler(): void {
$config = $this->server->get(IConfig::class);
if (!defined('PHPUNIT_RUN')) {
$errorHandler = new ErrorHandler(
$this->server->get(LoggerInterface::class),
);
$exceptionHandler = $errorHandler->onException(...);
if ($config->getSystemValueBool('debug', false)) {
set_error_handler($errorHandler->onAll(...), E_ALL);
if ($this->isCli()) {
$exceptionHandler = $this->server->get(ITemplateManager::class)->printExceptionErrorPage(...);
}
} else {
set_error_handler($errorHandler->onError(...));
}
register_shutdown_function($errorHandler->onShutdown(...));
set_exception_handler($exceptionHandler);
}
}
protected function setupServerContainer(): void {
// Enable lazy loading if activated
SimpleContainer::$useLazyObjects = (bool)$this->config->getValue('enable_lazy_objects', true);
$this->server = new Server($this->webRoot, $this->config, $this);
$this->server->boot();
\OC::$server = $this->server;
try {
$profiler = new BuiltInProfiler(
$this->server->get(IConfig::class),
$this->server->get(IRequest::class),
);
$profiler->start();
} catch (\Throwable $e) {
logger('core')->error('Failed to start profiler: ' . $e->getMessage(), ['app' => 'base']);
}
}
protected function setupLogging(): void {
if ($this->isCli() && in_array('--' . ReservedOptions::DEBUG_LOG, $_SERVER['argv'])) {
BeforeMessageLoggedEventListener::setup();
}
// Override php.ini and log everything if we're troubleshooting
if ($this->config->getValue('loglevel') === ILogger::DEBUG) {
error_reporting(E_ALL);
}
}
private function setupPhpDefault(): void {
// 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');
}
// 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 HintException('The PHP SimpleXML/PHP-XML extension is not installed.', 'Install the extension or make sure it is enabled.');
}
}
abstract protected function setupSession(IRequest $request, IEventLogger $eventLogger): void;
abstract public function checkInstalled(\OC\SystemConfig $systemConfig): void;
/**
* register hooks for the cleanup of cache and bruteforce protection
*/
public function registerCleanupHooks(\OC\SystemConfig $systemConfig): void {
//don't try to do this before we are properly setup
if ($systemConfig->getValue('installed', false) && !Util::needUpgrade()) {
// NOTE: This will be replaced to use OCP
$userSession = $this->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 = $this->server->get(IRequest::class);
$throttler = $this->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
$this->server->get(LoggerInterface::class)->warning('Exception when running cache gc.', [
'app' => 'core',
'exception' => $e,
]);
}
});
}
}
private function registerEncryptionWrapperAndHooks(): void {
/** @var \OC\Encryption\Manager */
$manager = $this->server->get(\OCP\Encryption\IManager::class);
$this->server->get(IEventDispatcher::class)->addListener(
BeforeFileSystemSetupEvent::class,
$manager->setupStorage(...),
);
$enabled = $manager->isEnabled();
if ($enabled) {
\OC\Encryption\EncryptionEventListener::register($this->server->get(IEventDispatcher::class));
}
}
private function registerAccountHooks(): void {
/** @var IEventDispatcher $dispatcher */
$dispatcher = $this->server->get(IEventDispatcher::class);
$dispatcher->addServiceListener(UserChangedEvent::class, \OC\Accounts\Hooks::class);
}
private function registerAppRestrictionsHooks(): void {
/** @var \OC\Group\Manager $groupManager */
$groupManager = $this->server->get(\OCP\IGroupManager::class);
$groupManager->listen('\OC\Group', 'postDelete', function (\OCP\IGroup $group) {
$appManager = $this->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 function registerResourceCollectionHooks(): void {
\OC\Collaboration\Resources\Listener::register($this->server->get(IEventDispatcher::class));
}
private function registerFileReferenceEventListener(): void {
\OC\Collaboration\Reference\File\FileReferenceEventListener::register($this->server->get(IEventDispatcher::class));
}
private function registerRenderReferenceEventListener() {
\OC\Collaboration\Reference\RenderReferenceEventListener::register($this->server->get(IEventDispatcher::class));
}
/**
* register hooks for sharing
*/
public function registerShareHooks(\OC\SystemConfig $systemConfig): void {
if ($systemConfig->getValue('installed')) {
$dispatcher = $this->server->get(IEventDispatcher::class);
$dispatcher->addServiceListener(UserRemovedEvent::class, UserRemovedListener::class);
$dispatcher->addServiceListener(GroupDeletedEvent::class, GroupDeletedListener::class);
$dispatcher->addServiceListener(UserDeletedEvent::class, UserDeletedListener::class);
}
}
}
@@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH
* SPDX-FileContributor: Carl Schwan
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Kernel;
use OCP\App\IAppManager;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IAppConfig;
use OCP\IDBConnection;
use OCP\IInitialStateService;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\Server;
use OCP\ServerVersion;
use OCP\Util;
use Throwable;
class UpgradePageController {
public function __construct(
private readonly IAppManager $appManager,
private readonly IDBConnection $connection,
private readonly IUserManager $userManager,
private readonly IInitialStateService $initialState,
private readonly IUrlGenerator $urlGenerator,
private readonly ServerVersion $serverVersion,
private readonly IAppConfig $appConfig,
) {
}
/**
* Prints the upgrade page
*/
public function printUpgradePage(\OC\SystemConfig $systemConfig): Response {
$cliUpgradeLink = $systemConfig->getValue('upgrade.cli-upgrade-link', '');
$disableWebUpdater = $systemConfig->getValue('upgrade.disable-web', false);
$tooBig = false;
if (!$disableWebUpdater) {
if ($this->appManager->isEnabledForAnyone('user_ldap')) {
$qb = $this->connection->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 && $this->appManager->isEnabledForAnyone('user_saml')) {
$qb = $this->connection->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 = $this->userManager->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');
$serverVersion = \OCP\Server::get(\OCP\ServerVersion::class);
if ($disableWebUpdater || ($tooBig && !$ignoreTooBigWarning)) {
// send http status 503
http_response_code(503);
header('Retry-After: 120');
$this->initialState->provideInitialState('core', 'updaterView', 'adminCli');
$this->initialState->provideInitialState('core', 'updateInfo', [
'cliUpgradeLink' => $cliUpgradeLink ?: $this->urlGenerator->linkToDocs('admin-cli-upgrade'),
'productName' => self::getProductName(),
'version' => $serverVersion->getVersionString(),
'tooBig' => $tooBig,
]);
// render error page
return new TemplateResponse('', 'update', [], TemplateResponse::RENDER_AS_GUEST);
}
// 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);
}
$appsToUpgrade = array_map(function (array $app): array {
return [
'id' => $app['id'],
'name' => $app['name'],
'version' => $app['version'],
'oldVersion' => $this->appConfig->getValueString($app['id'], 'installed_version'),
];
}, $appManager->getAppsNeedingUpgrade($ocVersion));
$params = [
'appsToUpgrade' => $appsToUpgrade,
'incompatibleAppsList' => $incompatibleDisabledApps,
'isAppsOnlyUpgrade' => $isAppsOnlyUpgrade,
'oldTheme' => $oldTheme,
'productName' => self::getProductName(),
'version' => $serverVersion->getVersionString(),
];
$this->initialState->provideInitialState('core', 'updaterView', 'admin');
$this->initialState->provideInitialState('core', 'updateInfo', $params);
return new TemplateResponse('', 'update', [], TemplateResponse::RENDER_AS_GUEST);
}
private static function getProductName(): string {
$productName = 'Nextcloud';
try {
$defaults = new \OC_Defaults();
$productName = $defaults->getName();
} catch (Throwable $error) {
// ignore
}
return $productName;
}
}
+5 -4
View File
@@ -13,6 +13,7 @@ use OCP\App\AppPathNotFoundException;
use OCP\App\IAppManager;
use OCP\AppFramework\App;
use OCP\AppFramework\Http\Attribute\Route as RouteAttribute;
use OCP\AppFramework\Http\Response;
use OCP\Diagnostics\IEventLogger;
use OCP\IConfig;
use OCP\IRequest;
@@ -307,10 +308,9 @@ class Router implements IRouter {
*
* @param string $url The url to find
* @throws \Exception
* @return void
*/
public function match($url) {
$parameters = $this->findMatchingRoute($url);
public function match(IRequest $request): Response {
$parameters = $this->findMatchingRoute($request->getPathInfo());
$this->eventLogger->start('route:run', 'Run route');
if (isset($parameters['caller'])) {
@@ -318,7 +318,7 @@ class Router implements IRouter {
unset($parameters['caller']);
unset($parameters['action']);
$application = $this->getApplicationClass($caller[0]);
\OC\AppFramework\App::main($caller[1], $caller[2], $application->getContainer(), $parameters);
return \OC\AppFramework\App::main($caller[1], $caller[2], $application->getContainer(), $parameters);
} elseif (isset($parameters['action'])) {
$this->logger->warning('Deprecated action route used', ['parameters' => $parameters]);
$this->callLegacyActionRoute($parameters);
@@ -328,6 +328,7 @@ class Router implements IRouter {
} else {
throw new \Exception('no action available');
}
//FIXME what about action and file, should we return a callback response?
$this->eventLogger->end('route:run');
}
+12 -10
View File
@@ -83,6 +83,7 @@ use OC\Http\Client\NegativeDnsCache;
use OC\IntegrityCheck\Checker;
use OC\IntegrityCheck\Helpers\EnvironmentHelper;
use OC\IntegrityCheck\Helpers\FileAccessHelper;
use OC\Kernel\Kernel;
use OC\KnownUser\KnownUserService;
use OC\LDAP\NullLDAPProviderFactory;
use OC\Lock\DBLockingProvider;
@@ -313,12 +314,13 @@ class Server extends ServerContainer implements IServerContainer {
public function __construct(
private string $webRoot,
Config $config,
Kernel $kernel,
) {
parent::__construct();
// To find out if we are running from CLI or not
$this->registerParameter('isCLI', \OC::$CLI);
$this->registerParameter('serverRoot', \OC::$SERVERROOT);
$this->registerParameter('isCLI', $kernel->isCli());
$this->registerParameter('serverRoot', $kernel->getServerRoot());
$this->registerService(ContainerInterface::class, function (ContainerInterface $c) {
return $c;
@@ -626,13 +628,13 @@ class Server extends ServerContainer implements IServerContainer {
$this->registerAlias(IUserConfig::class, UserConfig::class);
$this->registerAlias(IAppManager::class, AppManager::class);
$this->registerService(IFactory::class, function (Server $c) {
$this->registerService(IFactory::class, function (Server $c) use ($kernel) {
return new \OC\L10N\Factory(
$c->get(IConfig::class),
$c->getRequest(),
$c->get(IRequest::class),
$c->get(IUserSession::class),
$c->get(ICacheFactory::class),
\OC::$SERVERROOT,
$kernel->getServerRoot(),
$c->get(IAppManager::class),
);
});
@@ -975,7 +977,7 @@ class Server extends ServerContainer implements IServerContainer {
$factory = $c->get(ILDAPProviderFactory::class);
return $factory->getLDAPProvider();
});
$this->registerService(ILockingProvider::class, function (ContainerInterface $c) {
$this->registerService(ILockingProvider::class, function (ContainerInterface $c) use ($kernel) {
$ini = $c->get(IniGetWrapper::class);
$config = $c->get(IConfig::class);
$ttl = $config->getSystemValueInt('filelocking.ttl', max(3600, (int)($ini->getNumeric('max_execution_time') ?? 0)));
@@ -991,7 +993,7 @@ class Server extends ServerContainer implements IServerContainer {
$c->get(IDBConnection::class),
new TimeFactory(),
$ttl,
!\OC::$CLI
!$kernel->isCli(),
);
}
return new NoopLockingProvider();
@@ -1008,12 +1010,12 @@ class Server extends ServerContainer implements IServerContainer {
});
$this->registerAlias(IMountManager::class, \OC\Files\Mount\Manager::class);
$this->registerService(IMimeTypeDetector::class, function (ContainerInterface $c) {
$this->registerService(IMimeTypeDetector::class, function (ContainerInterface $c) use ($kernel) {
return new Detection(
$c->get(IURLGenerator::class),
$c->get(LoggerInterface::class),
\OC::$configDir,
\OC::$SERVERROOT . '/resources/config/'
$kernel->getConfigDir(),
$kernel->getServerRoot() . '/resources/config/'
);
});
+2 -1
View File
@@ -12,6 +12,7 @@ namespace OC\Session;
use OC\Authentication\Token\IProvider;
use OC\Diagnostics\TLogSlowOperation;
use OCP\Authentication\Exceptions\InvalidTokenException;
use OCP\IConfig;
use OCP\Server;
use OCP\Session\Exceptions\SessionNotAvailableException;
use Psr\Log\LoggerInterface;
@@ -213,7 +214,7 @@ class Internal extends Session {
private function startSession(bool $silence = false, bool $readAndClose = true) {
$sessionParams = ['cookie_samesite' => 'Lax'];
if (\OC::hasSessionRelaxedExpiry()) {
if (Server::get(IConfig::class)->getSystemValueBool('session_relaxed_expiry', false)) {
$sessionParams['read_and_close'] = $readAndClose;
}
$this->invoke('session_start', [$sessionParams], $silence);
+4 -3
View File
@@ -12,6 +12,7 @@ use OC\App\DependencyAnalyzer;
use OC\AppFramework\App;
use OC\AppFramework\Bootstrap\Coordinator;
use OC\Installer;
use OC\Kernel\Kernel;
use OC\NeedsUpdateException;
use OC\Repair;
use OC\Repair\Events\RepairErrorEvent;
@@ -113,17 +114,17 @@ class OC_App {
// Register on PSR-4 composer autoloader
$appNamespace = App::buildAppNamespace($app);
\OC::$server->registerNamespace($app, $appNamespace);
Kernel::getInstance()->getServer()->registerNamespace($app, $appNamespace);
if (file_exists($path . '/composer/autoload.php')) {
require_once $path . '/composer/autoload.php';
} else {
\OC::$composerAutoloader->addPsr4($appNamespace . '\\', $path . '/lib/', true);
Kernel::getInstance()->addPsr4($appNamespace . '\\', $path . '/lib/', true);
}
// Register Test namespace only when testing
if (defined('PHPUNIT_RUN') || defined('CLI_TEST_RUN')) {
\OC::$composerAutoloader->addPsr4($appNamespace . '\\Tests\\', $path . '/tests/', true);
Kernel::getInstance()->addPsr4($appNamespace . '\\Tests\\', $path . '/tests/', true);
}
}
+5 -2
View File
@@ -5,8 +5,11 @@
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
use OC\Kernel\Kernel;
use OCP\IConfig;
use OCP\IURLGenerator;
use OCP\L10N\IFactory;
use OCP\Server;
use OCP\ServerVersion;
use OCP\Util;
@@ -51,7 +54,7 @@ class OC_Defaults {
$this->defaultTextColorPrimary = '#ffffff';
$this->defaultProductName = 'Nextcloud';
$themePath = OC::$SERVERROOT . '/themes/' . OC_Util::getTheme() . '/defaults.php';
$themePath = Kernel::getInstance()->getServerRoot() . '/themes/' . OC_Util::getTheme() . '/defaults.php';
if (file_exists($themePath)) {
// prevent defaults.php from printing output
ob_start();
@@ -214,7 +217,7 @@ class OC_Defaults {
return $this->theme->getSlogan($lang);
} else {
if ($this->defaultSlogan === null) {
$l10n = \OC::$server->getL10N('lib', $lang);
$l10n = \OCP\Server::get(IFactory::class)->get('lib', $lang);
$this->defaultSlogan = $l10n->t('a safe home for all your data');
}
return $this->defaultSlogan;
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH
* SPDX-FileContributor: Carl Schwan
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCP\AppFramework\Http;
use OC\App\AppManager;
use OCP\AppFramework\Http;
use OCP\Server;
class ErrorTemplateResponse extends TemplateResponse {
public function __construct(
string $errorMessage,
string $hint,
string $renderAs = self::RENDER_AS_USER,
int $status = Http::STATUS_INTERNAL_SERVER_ERROR,
array $headers = []
) {
if ($errorMessage === $hint) {
// If the hint is the same as the message there is no need to display it twice.
$hint = '';
}
$errors = [['error' => $errorMessage, 'hint' => $hint]];
$params = ['errors' => $errors];
$appManager = Server::get(AppManager::class);
if ($appManager->isEnabledForUser('theming') && !$appManager->isAppLoaded('theming')) {
$appManager->loadApp('theming');
}
parent::__construct('', 'error', $params, $renderAs, $status, $headers);
}
}
+2
View File
@@ -7,6 +7,7 @@ declare(strict_types=1);
*/
namespace OCP;
use OC\Kernel\Kernel;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
@@ -33,6 +34,7 @@ final class Server {
* @since 25.0.0
*/
public static function get(string $serviceName) {
return Kernel::getInstance()->getContainer()->get($serviceName);
/** @psalm-suppress UndefinedClass */
return \OC::$server->get($serviceName);
}