diff options
Diffstat (limited to 'lib/private/AppFramework')
33 files changed, 709 insertions, 937 deletions
diff --git a/lib/private/AppFramework/App.php b/lib/private/AppFramework/App.php index 9f9fb32dbcb..7bf32852209 100644 --- a/lib/private/AppFramework/App.php +++ b/lib/private/AppFramework/App.php @@ -37,7 +37,7 @@ class App { * namespace tag or uppercasing the appid's first letter * @param string $appId the app id * @param string $topNamespace the namespace which should be prepended to - * the transformed app id, defaults to OCA\ + * the transformed app id, defaults to OCA\ * @return string the starting namespace for the app */ public static function buildAppNamespace(string $appId, string $topNamespace = 'OCA\\'): string { @@ -50,19 +50,8 @@ class App { if (isset($appInfo['namespace'])) { self::$nameSpaceCache[$appId] = trim($appInfo['namespace']); } else { - if ($appId !== 'spreed') { - // if the tag is not found, fall back to uppercasing the first letter - self::$nameSpaceCache[$appId] = ucfirst($appId); - } else { - // For the Talk app (appid spreed) the above fallback doesn't work. - // This leads to a problem when trying to install it freshly, - // because the apps namespace is already registered before the - // app is downloaded from the appstore, because of the hackish - // global route index.php/call/{token} which is registered via - // the core/routes.php so it does not have the app namespace. - // @ref https://github.com/nextcloud/server/pull/19433 - self::$nameSpaceCache[$appId] = 'Talk'; - } + // if the tag is not found, fall back to uppercasing the first letter + self::$nameSpaceCache[$appId] = ucfirst($appId); } return $topNamespace . self::$nameSpaceCache[$appId]; @@ -82,7 +71,6 @@ class App { return null; } - /** * Shortcut for calling a controller method and printing the result * @@ -93,7 +81,12 @@ class App { * @param array $urlParams list of URL parameters (optional) * @throws HintException */ - public static function main(string $controllerName, string $methodName, DIContainer $container, ?array $urlParams = null) { + public static function main( + string $controllerName, + string $methodName, + DIContainer $container, + ?array $urlParams = null, + ): void { /** @var IProfiler $profiler */ $profiler = $container->get(IProfiler::class); $eventLogger = $container->get(IEventLogger::class); @@ -101,7 +94,7 @@ class App { $profiler->setEnabled($profiler->isEnabled() && !is_null($urlParams) && isset($urlParams['_route']) && !str_starts_with($urlParams['_route'], 'profiler.')); if ($profiler->isEnabled()) { \OC::$server->get(IEventLogger::class)->activate(); - $profiler->add(new RoutingDataCollector($container['AppName'], $controllerName, $methodName)); + $profiler->add(new RoutingDataCollector($container['appName'], $controllerName, $methodName)); } $eventLogger->start('app:controller:params', 'Gather controller parameters'); @@ -115,7 +108,7 @@ class App { $request = $container->get(IRequest::class); $request->setUrlParameters($container['urlParams']); } - $appName = $container['AppName']; + $appName = $container['appName']; $eventLogger->end('app:controller:params'); @@ -145,8 +138,7 @@ class App { $eventLogger->start('app:controller:dispatcher', 'Initialize dispatcher and pre-middleware'); // initialize the dispatcher and run all the middleware before the controller - /** @var Dispatcher $dispatcher */ - $dispatcher = $container['Dispatcher']; + $dispatcher = $container->get(Dispatcher::class); $eventLogger->end('app:controller:dispatcher'); @@ -222,25 +214,4 @@ class App { } } } - - /** - * Shortcut for calling a controller method and printing the result. - * Similar to App:main except that no headers will be sent. - * - * @param string $controllerName the name of the controller under which it is - * stored in the DI container - * @param string $methodName the method that you want to call - * @param array $urlParams an array with variables extracted from the routes - * @param DIContainer $container an instance of a pimple container. - */ - public static function part(string $controllerName, string $methodName, array $urlParams, - DIContainer $container) { - $container['urlParams'] = $urlParams; - $controller = $container[$controllerName]; - - $dispatcher = $container['Dispatcher']; - - [, , $output] = $dispatcher->dispatch($controller, $methodName); - return $output; - } } diff --git a/lib/private/AppFramework/Bootstrap/Coordinator.php b/lib/private/AppFramework/Bootstrap/Coordinator.php index 4e78450fa04..a31dd6a05e1 100644 --- a/lib/private/AppFramework/Bootstrap/Coordinator.php +++ b/lib/private/AppFramework/Bootstrap/Coordinator.php @@ -20,6 +20,7 @@ use OCP\Dashboard\IManager; use OCP\Diagnostics\IEventLogger; use OCP\EventDispatcher\IEventDispatcher; use OCP\IServerContainer; +use Psr\Container\ContainerExceptionInterface; use Psr\Log\LoggerInterface; use Throwable; use function class_exists; @@ -30,8 +31,8 @@ class Coordinator { /** @var RegistrationContext|null */ private $registrationContext; - /** @var string[] */ - private $bootedApps = []; + /** @var array<string,true> */ + private array $bootedApps = []; public function __construct( private IServerContainer $serverContainer, @@ -45,7 +46,13 @@ class Coordinator { } public function runInitialRegistration(): void { - $this->registerApps(OC_App::getEnabledApps()); + $apps = OC_App::getEnabledApps(); + if (!empty($apps)) { + // make sure to also register the core app + $apps = ['core', ...$apps]; + } + + $this->registerApps($apps); } public function runLazyRegistration(string $appId): void { @@ -69,26 +76,32 @@ class Coordinator { */ try { $path = $this->appManager->getAppPath($appId); + OC_App::registerAutoloading($appId, $path); } catch (AppPathNotFoundException) { // Ignore continue; } - OC_App::registerAutoloading($appId, $path); $this->eventLogger->end("bootstrap:register_app:$appId:autoloader"); /* * Next we check if there is an application class, and it implements * the \OCP\AppFramework\Bootstrap\IBootstrap interface */ - $appNameSpace = App::buildAppNamespace($appId); + if ($appId === 'core') { + $appNameSpace = 'OC\\Core'; + } else { + $appNameSpace = App::buildAppNamespace($appId); + } $applicationClassName = $appNameSpace . '\\AppInfo\\Application'; + try { - if (class_exists($applicationClassName) && in_array(IBootstrap::class, class_implements($applicationClassName), true)) { + if (class_exists($applicationClassName) && is_a($applicationClassName, IBootstrap::class, true)) { $this->eventLogger->start("bootstrap:register_app:$appId:application", "Load `Application` instance for $appId"); try { - /** @var IBootstrap|App $application */ - $apps[$appId] = $application = $this->serverContainer->query($applicationClassName); - } catch (QueryException $e) { + /** @var IBootstrap&App $application */ + $application = $this->serverContainer->query($applicationClassName); + $apps[$appId] = $application; + } catch (ContainerExceptionInterface $e) { // Weird, but ok $this->eventLogger->end("bootstrap:register_app:$appId"); continue; @@ -171,7 +184,7 @@ class Coordinator { public function isBootable(string $appId) { $appNameSpace = App::buildAppNamespace($appId); $applicationClassName = $appNameSpace . '\\AppInfo\\Application'; - return class_exists($applicationClassName) && - in_array(IBootstrap::class, class_implements($applicationClassName), true); + return class_exists($applicationClassName) + && in_array(IBootstrap::class, class_implements($applicationClassName), true); } } diff --git a/lib/private/AppFramework/Bootstrap/RegistrationContext.php b/lib/private/AppFramework/Bootstrap/RegistrationContext.php index df03d59ebfa..8bd1ff35610 100644 --- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php +++ b/lib/private/AppFramework/Bootstrap/RegistrationContext.php @@ -21,11 +21,13 @@ use OCP\Calendar\Resource\IBackend as IResourceBackend; use OCP\Calendar\Room\IBackend as IRoomBackend; use OCP\Capabilities\ICapability; use OCP\Collaboration\Reference\IReferenceProvider; +use OCP\Config\Lexicon\ILexicon; use OCP\Dashboard\IManager; use OCP\Dashboard\IWidget; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Template\ICustomTemplateProvider; use OCP\Http\WellKnown\IHandler; +use OCP\Mail\Provider\IProvider as IMailProvider; use OCP\Notification\INotifier; use OCP\Profile\ILinkAction; use OCP\Search\IProvider; @@ -140,6 +142,9 @@ class RegistrationContext { /** @var ServiceRegistration<IDeclarativeSettingsForm>[] */ private array $declarativeSettings = []; + /** @var array<array-key, string> */ + private array $configLexiconClasses = []; + /** @var ServiceRegistration<ITeamResourceProvider>[] */ private array $teamResourceProviders = []; @@ -149,6 +154,12 @@ class RegistrationContext { /** @var ServiceRegistration<\OCP\TaskProcessing\ITaskType>[] */ private array $taskProcessingTaskTypes = []; + /** @var ServiceRegistration<\OCP\Files\Conversion\IConversionProvider>[] */ + private array $fileConversionProviders = []; + + /** @var ServiceRegistration<IMailProvider>[] */ + private $mailProviders = []; + public function __construct(LoggerInterface $logger) { $this->logger = $logger; } @@ -411,6 +422,27 @@ class RegistrationContext { $taskProcessingTaskTypeClass ); } + + public function registerFileConversionProvider(string $class): void { + $this->context->registerFileConversionProvider( + $this->appId, + $class + ); + } + + public function registerMailProvider(string $class): void { + $this->context->registerMailProvider( + $this->appId, + $class + ); + } + + public function registerConfigLexicon(string $configLexiconClass): void { + $this->context->registerConfigLexicon( + $this->appId, + $configLexiconClass + ); + } }; } @@ -527,10 +559,10 @@ class RegistrationContext { public function registerTalkBackend(string $appId, string $backend) { // Some safeguards for invalid registrations if ($appId !== 'spreed') { - throw new RuntimeException("Only the Talk app is allowed to register a Talk backend"); + throw new RuntimeException('Only the Talk app is allowed to register a Talk backend'); } if ($this->talkBackendRegistration !== null) { - throw new RuntimeException("There can only be one Talk backend"); + throw new RuntimeException('There can only be one Talk backend'); } $this->talkBackendRegistration = new ServiceRegistration($appId, $backend); @@ -605,6 +637,27 @@ class RegistrationContext { } /** + * @psalm-param class-string<\OCP\Files\Conversion\IConversionProvider> $class + */ + public function registerFileConversionProvider(string $appId, string $class): void { + $this->fileConversionProviders[] = new ServiceRegistration($appId, $class); + } + + /** + * @psalm-param class-string<IMailProvider> $migratorClass + */ + public function registerMailProvider(string $appId, string $class): void { + $this->mailProviders[] = new ServiceRegistration($appId, $class); + } + + /** + * @psalm-param class-string<ILexicon> $configLexiconClass + */ + public function registerConfigLexicon(string $appId, string $configLexiconClass): void { + $this->configLexiconClasses[$appId] = $configLexiconClass; + } + + /** * @param App[] $apps */ public function delegateCapabilityRegistrations(array $apps): void { @@ -948,4 +1001,34 @@ class RegistrationContext { public function getTaskProcessingTaskTypes(): array { return $this->taskProcessingTaskTypes; } + + /** + * @return ServiceRegistration<\OCP\Files\Conversion\IConversionProvider>[] + */ + public function getFileConversionProviders(): array { + return $this->fileConversionProviders; + } + + /** + * @return ServiceRegistration<IMailProvider>[] + */ + public function getMailProviders(): array { + return $this->mailProviders; + } + + /** + * returns IConfigLexicon registered by the app. + * null if none registered. + * + * @param string $appId + * + * @return ILexicon|null + */ + public function getConfigLexicon(string $appId): ?ILexicon { + if (!array_key_exists($appId, $this->configLexiconClasses)) { + return null; + } + + return \OCP\Server::get($this->configLexiconClasses[$appId]); + } } diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index 4add17396b0..76261fe6b92 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -1,19 +1,33 @@ <?php + +declare(strict_types=1); + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-only */ + namespace OC\AppFramework\DependencyInjection; -use OC; use OC\AppFramework\Http; use OC\AppFramework\Http\Dispatcher; use OC\AppFramework\Http\Output; +use OC\AppFramework\Middleware\AdditionalScriptsMiddleware; +use OC\AppFramework\Middleware\CompressionMiddleware; +use OC\AppFramework\Middleware\FlowV2EphemeralSessionsMiddleware; use OC\AppFramework\Middleware\MiddlewareDispatcher; +use OC\AppFramework\Middleware\NotModifiedMiddleware; use OC\AppFramework\Middleware\OCSMiddleware; +use OC\AppFramework\Middleware\PublicShare\PublicShareMiddleware; +use OC\AppFramework\Middleware\Security\BruteForceMiddleware; use OC\AppFramework\Middleware\Security\CORSMiddleware; +use OC\AppFramework\Middleware\Security\CSPMiddleware; +use OC\AppFramework\Middleware\Security\FeaturePolicyMiddleware; +use OC\AppFramework\Middleware\Security\PasswordConfirmationMiddleware; use OC\AppFramework\Middleware\Security\RateLimitingMiddleware; +use OC\AppFramework\Middleware\Security\ReloadExecutionMiddleware; +use OC\AppFramework\Middleware\Security\SameSiteCookieMiddleware; use OC\AppFramework\Middleware\Security\SecurityMiddleware; use OC\AppFramework\Middleware\SessionMiddleware; use OC\AppFramework\ScopedPsrLogger; @@ -24,60 +38,45 @@ use OC\Log\PsrLoggerAdapter; use OC\ServerContainer; use OC\Settings\AuthorizedGroupMapper; use OCA\WorkflowEngine\Manager; +use OCP\App\IAppManager; use OCP\AppFramework\Http\IOutput; use OCP\AppFramework\IAppContainer; use OCP\AppFramework\QueryException; use OCP\AppFramework\Services\IAppConfig; use OCP\AppFramework\Services\IInitialState; use OCP\AppFramework\Utility\IControllerMethodReflector; -use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Files\AppData\IAppDataFactory; use OCP\Files\Folder; use OCP\Files\IAppData; use OCP\Group\ISubAdmin; use OCP\IConfig; use OCP\IDBConnection; -use OCP\IInitialStateService; +use OCP\IGroupManager; use OCP\IL10N; -use OCP\ILogger; use OCP\INavigationManager; use OCP\IRequest; use OCP\IServerContainer; use OCP\ISession; use OCP\IURLGenerator; use OCP\IUserSession; -use OCP\Security\Bruteforce\IThrottler; +use OCP\Security\Ip\IRemoteAddress; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; -/** - * @deprecated 20.0.0 - */ class DIContainer extends SimpleContainer implements IAppContainer { - private string $appName; + protected string $appName; + private array $middleWares = []; + private ServerContainer $server; - /** - * @var array - */ - private $middleWares = []; - - /** @var ServerContainer */ - private $server; - - /** - * Put your class dependencies in here - * @param string $appName the name of the app - * @param array $urlParams - * @param ServerContainer|null $server - */ public function __construct(string $appName, array $urlParams = [], ?ServerContainer $server = null) { parent::__construct(); $this->appName = $appName; - $this['appName'] = $appName; - $this['urlParams'] = $urlParams; + $this->registerParameter('appName', $appName); + $this->registerParameter('urlParams', $urlParams); - $this->registerAlias('Request', IRequest::class); + /** @deprecated 32.0.0 */ + $this->registerDeprecatedAlias('Request', IRequest::class); - /** @var \OC\ServerContainer $server */ if ($server === null) { $server = \OC::$server; } @@ -85,80 +84,76 @@ class DIContainer extends SimpleContainer implements IAppContainer { $this->server->registerAppContainer($appName, $this); // aliases - /** @deprecated inject $appName */ - $this->registerAlias('AppName', 'appName'); - /** @deprecated inject $webRoot*/ - $this->registerAlias('WebRoot', 'webRoot'); - /** @deprecated inject $userId */ - $this->registerAlias('UserId', 'userId'); + /** @deprecated 26.0.0 inject $appName */ + $this->registerDeprecatedAlias('AppName', 'appName'); + /** @deprecated 26.0.0 inject $webRoot*/ + $this->registerDeprecatedAlias('WebRoot', 'webRoot'); + /** @deprecated 26.0.0 inject $userId */ + $this->registerDeprecatedAlias('UserId', 'userId'); /** * Core services */ - $this->registerService(IOutput::class, function () { - return new Output($this->getServer()->getWebRoot()); - }); + /* Cannot be an alias because Output is not in OCA */ + $this->registerService(IOutput::class, fn (ContainerInterface $c): IOutput => new Output($c->get('webRoot'))); $this->registerService(Folder::class, function () { return $this->getServer()->getUserFolder(); }); - $this->registerService(IAppData::class, function (ContainerInterface $c) { - return $this->getServer()->getAppDataDir($c->get('AppName')); + $this->registerService(IAppData::class, function (ContainerInterface $c): IAppData { + return $c->get(IAppDataFactory::class)->get($c->get('appName')); }); $this->registerService(IL10N::class, function (ContainerInterface $c) { - return $this->getServer()->getL10N($c->get('AppName')); + return $this->getServer()->getL10N($c->get('appName')); }); // Log wrappers $this->registerService(LoggerInterface::class, function (ContainerInterface $c) { + /* Cannot be an alias because it uses LoggerInterface so it would infinite loop */ return new ScopedPsrLogger( $c->get(PsrLoggerAdapter::class), - $c->get('AppName') + $c->get('appName') ); }); - $this->registerService(ILogger::class, function (ContainerInterface $c) { - return new OC\AppFramework\Logger($this->server->query(ILogger::class), $c->get('AppName')); - }); $this->registerService(IServerContainer::class, function () { return $this->getServer(); }); - $this->registerAlias('ServerContainer', IServerContainer::class); + /** @deprecated 32.0.0 */ + $this->registerDeprecatedAlias('ServerContainer', IServerContainer::class); - $this->registerService(\OCP\WorkflowEngine\IManager::class, function (ContainerInterface $c) { - return $c->get(Manager::class); - }); + $this->registerAlias(\OCP\WorkflowEngine\IManager::class, Manager::class); - $this->registerService(ContainerInterface::class, function (ContainerInterface $c) { - return $c; - }); - $this->registerAlias(IAppContainer::class, ContainerInterface::class); + $this->registerService(ContainerInterface::class, fn (ContainerInterface $c) => $c); + $this->registerDeprecatedAlias(IAppContainer::class, ContainerInterface::class); // commonly used attributes - $this->registerService('userId', function (ContainerInterface $c) { - return $c->get(IUserSession::class)->getSession()->get('user_id'); + $this->registerService('userId', function (ContainerInterface $c): ?string { + return $c->get(ISession::class)->get('user_id'); }); - $this->registerService('webRoot', function (ContainerInterface $c) { + $this->registerService('webRoot', function (ContainerInterface $c): string { return $c->get(IServerContainer::class)->getWebRoot(); }); - $this->registerService('OC_Defaults', function (ContainerInterface $c) { + $this->registerService('OC_Defaults', function (ContainerInterface $c): object { return $c->get(IServerContainer::class)->get('ThemingDefaults'); }); - $this->registerService('Protocol', function (ContainerInterface $c) { - /** @var \OC\Server $server */ - $server = $c->get(IServerContainer::class); - $protocol = $server->getRequest()->getHttpProtocol(); + /** @deprecated 32.0.0 */ + $this->registerDeprecatedAlias('Protocol', Http::class); + $this->registerService(Http::class, function (ContainerInterface $c) { + $protocol = $c->get(IRequest::class)->getHttpProtocol(); return new Http($_SERVER, $protocol); }); - $this->registerService('Dispatcher', function (ContainerInterface $c) { + /** @deprecated 32.0.0 */ + $this->registerDeprecatedAlias('Dispatcher', Dispatcher::class); + $this->registerService(Dispatcher::class, function (ContainerInterface $c) { return new Dispatcher( - $c->get('Protocol'), + $c->get(Http::class), $c->get(MiddlewareDispatcher::class), $c->get(IControllerMethodReflector::class), $c->get(IRequest::class), @@ -180,123 +175,52 @@ class DIContainer extends SimpleContainer implements IAppContainer { /** * Middleware */ - $this->registerAlias('MiddlewareDispatcher', MiddlewareDispatcher::class); + /** @deprecated 32.0.0 */ + $this->registerDeprecatedAlias('MiddlewareDispatcher', MiddlewareDispatcher::class); $this->registerService(MiddlewareDispatcher::class, function (ContainerInterface $c) { $server = $this->getServer(); $dispatcher = new MiddlewareDispatcher(); - $dispatcher->registerMiddleware( - $c->get(OC\AppFramework\Middleware\CompressionMiddleware::class) - ); - - $dispatcher->registerMiddleware($c->get(OC\AppFramework\Middleware\NotModifiedMiddleware::class)); - - $dispatcher->registerMiddleware( - $c->get(OC\AppFramework\Middleware\Security\ReloadExecutionMiddleware::class) - ); - - $dispatcher->registerMiddleware( - new OC\AppFramework\Middleware\Security\SameSiteCookieMiddleware( - $c->get(IRequest::class), - $c->get(IControllerMethodReflector::class) - ) - ); - $dispatcher->registerMiddleware( - new CORSMiddleware( - $c->get(IRequest::class), - $c->get(IControllerMethodReflector::class), - $c->get(IUserSession::class), - $c->get(IThrottler::class) - ) - ); - $dispatcher->registerMiddleware( - new OCSMiddleware( - $c->get(IRequest::class) - ) - ); - + $dispatcher->registerMiddleware($c->get(CompressionMiddleware::class)); + $dispatcher->registerMiddleware($c->get(NotModifiedMiddleware::class)); + $dispatcher->registerMiddleware($c->get(ReloadExecutionMiddleware::class)); + $dispatcher->registerMiddleware($c->get(SameSiteCookieMiddleware::class)); + $dispatcher->registerMiddleware($c->get(CORSMiddleware::class)); + $dispatcher->registerMiddleware($c->get(OCSMiddleware::class)); + $dispatcher->registerMiddleware($c->get(FlowV2EphemeralSessionsMiddleware::class)); $securityMiddleware = new SecurityMiddleware( $c->get(IRequest::class), $c->get(IControllerMethodReflector::class), $c->get(INavigationManager::class), $c->get(IURLGenerator::class), - $server->get(LoggerInterface::class), - $c->get('AppName'), + $c->get(LoggerInterface::class), + $c->get('appName'), $server->getUserSession()->isLoggedIn(), - $this->getUserId() !== null && $server->getGroupManager()->isAdmin($this->getUserId()), - $server->getUserSession()->getUser() !== null && $server->query(ISubAdmin::class)->isSubAdmin($server->getUserSession()->getUser()), - $server->getAppManager(), + $c->get(IGroupManager::class), + $c->get(ISubAdmin::class), + $c->get(IAppManager::class), $server->getL10N('lib'), $c->get(AuthorizedGroupMapper::class), - $server->get(IUserSession::class) + $c->get(IUserSession::class), + $c->get(IRemoteAddress::class), ); $dispatcher->registerMiddleware($securityMiddleware); - $dispatcher->registerMiddleware( - new OC\AppFramework\Middleware\Security\CSPMiddleware( - $server->query(OC\Security\CSP\ContentSecurityPolicyManager::class), - $server->query(OC\Security\CSP\ContentSecurityPolicyNonceManager::class), - $server->query(OC\Security\CSRF\CsrfTokenManager::class) - ) - ); - $dispatcher->registerMiddleware( - $server->query(OC\AppFramework\Middleware\Security\FeaturePolicyMiddleware::class) - ); - $dispatcher->registerMiddleware( - new OC\AppFramework\Middleware\Security\PasswordConfirmationMiddleware( - $c->get(IControllerMethodReflector::class), - $c->get(ISession::class), - $c->get(IUserSession::class), - $c->get(ITimeFactory::class), - $c->get(\OC\Authentication\Token\IProvider::class), - ) - ); - $dispatcher->registerMiddleware( - new TwoFactorMiddleware( - $c->get(OC\Authentication\TwoFactorAuth\Manager::class), - $c->get(IUserSession::class), - $c->get(ISession::class), - $c->get(IURLGenerator::class), - $c->get(IControllerMethodReflector::class), - $c->get(IRequest::class) - ) - ); - $dispatcher->registerMiddleware( - new OC\AppFramework\Middleware\Security\BruteForceMiddleware( - $c->get(IControllerMethodReflector::class), - $c->get(IThrottler::class), - $c->get(IRequest::class), - $c->get(LoggerInterface::class) - ) - ); - $dispatcher->registerMiddleware( - new RateLimitingMiddleware( - $c->get(IRequest::class), - $c->get(IUserSession::class), - $c->get(IControllerMethodReflector::class), - $c->get(OC\Security\RateLimiting\Limiter::class), - $c->get(ISession::class) - ) - ); - $dispatcher->registerMiddleware( - new OC\AppFramework\Middleware\PublicShare\PublicShareMiddleware( - $c->get(IRequest::class), - $c->get(ISession::class), - $c->get(\OCP\IConfig::class), - $c->get(IThrottler::class) - ) - ); - $dispatcher->registerMiddleware( - $c->get(\OC\AppFramework\Middleware\AdditionalScriptsMiddleware::class) - ); + $dispatcher->registerMiddleware($c->get(CSPMiddleware::class)); + $dispatcher->registerMiddleware($c->get(FeaturePolicyMiddleware::class)); + $dispatcher->registerMiddleware($c->get(PasswordConfirmationMiddleware::class)); + $dispatcher->registerMiddleware($c->get(TwoFactorMiddleware::class)); + $dispatcher->registerMiddleware($c->get(BruteForceMiddleware::class)); + $dispatcher->registerMiddleware($c->get(RateLimitingMiddleware::class)); + $dispatcher->registerMiddleware($c->get(PublicShareMiddleware::class)); + $dispatcher->registerMiddleware($c->get(AdditionalScriptsMiddleware::class)); - /** @var \OC\AppFramework\Bootstrap\Coordinator $coordinator */ $coordinator = $c->get(\OC\AppFramework\Bootstrap\Coordinator::class); $registrationContext = $coordinator->getRegistrationContext(); if ($registrationContext !== null) { - $appId = $this->getAppName(); + $appId = $this->get('appName'); foreach ($registrationContext->getMiddlewareRegistrations() as $middlewareRegistration) { if ($middlewareRegistration->getAppId() === $appId || $middlewareRegistration->isGlobal()) { @@ -308,28 +232,12 @@ class DIContainer extends SimpleContainer implements IAppContainer { $dispatcher->registerMiddleware($c->get($middleWare)); } - $dispatcher->registerMiddleware( - new SessionMiddleware( - $c->get(IControllerMethodReflector::class), - $c->get(ISession::class) - ) - ); + $dispatcher->registerMiddleware($c->get(SessionMiddleware::class)); return $dispatcher; }); - $this->registerService(IAppConfig::class, function (ContainerInterface $c) { - return new OC\AppFramework\Services\AppConfig( - $c->get(IConfig::class), - $c->get(\OCP\IAppConfig::class), - $c->get('AppName') - ); - }); - $this->registerService(IInitialState::class, function (ContainerInterface $c) { - return new OC\AppFramework\Services\InitialState( - $c->get(IInitialStateService::class), - $c->get('AppName') - ); - }); + $this->registerAlias(IAppConfig::class, \OC\AppFramework\Services\AppConfig::class); + $this->registerAlias(IInitialState::class, \OC\AppFramework\Services\InitialState::class); } /** @@ -341,13 +249,13 @@ class DIContainer extends SimpleContainer implements IAppContainer { /** * @param string $middleWare - * @return boolean|null */ - public function registerMiddleWare($middleWare) { + public function registerMiddleWare($middleWare): bool { if (in_array($middleWare, $this->middleWares, true) !== false) { return false; } $this->middleWares[] = $middleWare; + return true; } /** @@ -355,11 +263,11 @@ class DIContainer extends SimpleContainer implements IAppContainer { * @return string the name of your application */ public function getAppName() { - return $this->query('AppName'); + return $this->query('appName'); } /** - * @deprecated use IUserSession->isLoggedIn() + * @deprecated 12.0.0 use IUserSession->isLoggedIn() * @return boolean */ public function isLoggedIn() { @@ -367,7 +275,7 @@ class DIContainer extends SimpleContainer implements IAppContainer { } /** - * @deprecated use IGroupManager->isAdmin($userId) + * @deprecated 12.0.0 use IGroupManager->isAdmin($userId) * @return boolean */ public function isAdminUser() { @@ -375,7 +283,7 @@ class DIContainer extends SimpleContainer implements IAppContainer { return \OC_User::isAdminUser($uid); } - private function getUserId() { + private function getUserId(): string { return $this->getServer()->getSession()->get('user_id'); } @@ -442,9 +350,15 @@ class DIContainer extends SimpleContainer implements IAppContainer { return parent::query($name); } elseif (str_starts_with($name, \OC\AppFramework\App::buildAppNamespace($this->appName) . '\\')) { return parent::query($name); + } elseif ( + str_starts_with($name, 'OC\\AppFramework\\Services\\') + || str_starts_with($name, 'OC\\AppFramework\\Middleware\\') + ) { + /* AppFramework services are scoped to the application */ + return parent::query($name); } - throw new QueryException('Could not resolve ' . $name . '!' . - ' Class can not be instantiated', 1); + throw new QueryException('Could not resolve ' . $name . '!' + . ' Class can not be instantiated', 1); } } diff --git a/lib/private/AppFramework/Http.php b/lib/private/AppFramework/Http.php index 3c3e19692bf..08d6259c2a2 100644 --- a/lib/private/AppFramework/Http.php +++ b/lib/private/AppFramework/Http.php @@ -102,7 +102,7 @@ class Http extends BaseHttp { $status = self::STATUS_FOUND; } - return $this->protocolVersion . ' ' . $status . ' ' . - $this->headers[$status]; + return $this->protocolVersion . ' ' . $status . ' ' + . $this->headers[$status]; } } diff --git a/lib/private/AppFramework/Http/Dispatcher.php b/lib/private/AppFramework/Http/Dispatcher.php index bbb68972a41..8d91ddf7502 100644 --- a/lib/private/AppFramework/Http/Dispatcher.php +++ b/lib/private/AppFramework/Http/Dispatcher.php @@ -55,16 +55,17 @@ class Dispatcher { /** * @param Http $protocol the http protocol with contains all status headers * @param MiddlewareDispatcher $middlewareDispatcher the dispatcher which - * runs the middleware + * runs the middleware * @param ControllerMethodReflector $reflector the reflector that is used to inject - * the arguments for the controller + * the arguments for the controller * @param IRequest $request the incoming request * @param IConfig $config * @param ConnectionAdapter $connection * @param LoggerInterface $logger * @param IEventLogger $eventLogger */ - public function __construct(Http $protocol, + public function __construct( + Http $protocol, MiddlewareDispatcher $middlewareDispatcher, ControllerMethodReflector $reflector, IRequest $request, @@ -72,7 +73,8 @@ class Dispatcher { ConnectionAdapter $connection, LoggerInterface $logger, IEventLogger $eventLogger, - ContainerInterface $appContainer) { + ContainerInterface $appContainer, + ) { $this->protocol = $protocol; $this->middlewareDispatcher = $middlewareDispatcher; $this->reflector = $reflector; @@ -89,10 +91,12 @@ class Dispatcher { * Handles a request and calls the dispatcher on the controller * @param Controller $controller the controller which will be called * @param string $methodName the method name which will be called on - * the controller - * @return array $array[0] contains a string with the http main header, - * $array[1] contains headers in the form: $key => value, $array[2] contains - * the response output + * the controller + * @return array $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 { @@ -183,16 +187,8 @@ class Dispatcher { $value = $this->request->getParam($param, $default); $type = $this->reflector->getType($param); - // if this is submitted using GET or a POST form, 'false' should be - // converted to false - if (($type === 'bool' || $type === 'boolean') && - $value === 'false' && - ( - $this->request->method === 'GET' || - str_contains($this->request->getHeader('Content-Type'), - 'application/x-www-form-urlencoded') - ) - ) { + // Converted the string `'false'` to false when the controller wants a boolean + if ($value === 'false' && ($type === 'bool' || $type === 'boolean')) { $value = false; } elseif ($value !== null && \in_array($type, $types, true)) { settype($value, $type); @@ -208,6 +204,10 @@ class Dispatcher { $response = \call_user_func_array([$controller, $methodName], $arguments); $this->eventLogger->end('controller:' . get_class($controller) . '::' . $methodName); + if (!($response instanceof Response)) { + $this->logger->debug($controller::class . '::' . $methodName . ' returned raw data. Please wrap it in a Response or one of it\'s inheritors.'); + } + // format response if ($response instanceof DataResponse || !($response instanceof Response)) { // get format from the url format or request format parameter diff --git a/lib/private/AppFramework/Http/Output.php b/lib/private/AppFramework/Http/Output.php index ff0ef1c15ca..b4a8672fdc7 100644 --- a/lib/private/AppFramework/Http/Output.php +++ b/lib/private/AppFramework/Http/Output.php @@ -13,14 +13,9 @@ use OCP\AppFramework\Http\IOutput; * Very thin wrapper class to make output testable */ class Output implements IOutput { - /** @var string */ - private $webRoot; - - /** - * @param $webRoot - */ - public function __construct($webRoot) { - $this->webRoot = $webRoot; + public function __construct( + private string $webRoot, + ) { } /** diff --git a/lib/private/AppFramework/Http/Request.php b/lib/private/AppFramework/Http/Request.php index 0bd430545d4..7cc7467675c 100644 --- a/lib/private/AppFramework/Http/Request.php +++ b/lib/private/AppFramework/Http/Request.php @@ -14,6 +14,7 @@ use OC\Security\TrustedDomainHelper; use OCP\IConfig; use OCP\IRequest; use OCP\IRequestId; +use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\IpUtils; /** @@ -38,13 +39,14 @@ class Request implements \ArrayAccess, \Countable, IRequest { public const USER_AGENT_CHROME = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\)( Ubuntu Chromium\/[0-9.]+|) Chrome\/[0-9.]+ (Mobile Safari|Safari)\/[0-9.]+( (Vivaldi|Brave|OPR)\/[0-9.]+|)$/'; // Safari User Agent from http://www.useragentstring.com/pages/Safari/ public const USER_AGENT_SAFARI = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Version\/[0-9.]+ Safari\/[0-9.A-Z]+$/'; + public const USER_AGENT_SAFARI_MOBILE = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Version\/[0-9.]+ (Mobile\/[0-9.A-Z]+) Safari\/[0-9.A-Z]+$/'; // Android Chrome user agent: https://developers.google.com/chrome/mobile/docs/user-agent public const USER_AGENT_ANDROID_MOBILE_CHROME = '#Android.*Chrome/[.0-9]*#'; public const USER_AGENT_FREEBOX = '#^Mozilla/5\.0$#'; public const REGEX_LOCALHOST = '/^(127\.0\.0\.1|localhost|\[::1\])$/'; protected string $inputStream; - protected $content; + private bool $isPutStreamContentAlreadySent = false; protected array $items = []; protected array $allowedKeys = [ 'get', @@ -63,18 +65,19 @@ class Request implements \ArrayAccess, \Countable, IRequest { protected ?CsrfTokenManager $csrfTokenManager; protected bool $contentDecoded = false; + private ?\JsonException $decodingException = null; /** * @param array $vars An associative array with the following optional values: - * - array 'urlParams' the parameters which were matched from the URL - * - array 'get' the $_GET array - * - array|string 'post' the $_POST array or JSON string - * - array 'files' the $_FILES array - * - array 'server' the $_SERVER array - * - array 'env' the $_ENV array - * - array 'cookies' the $_COOKIE array - * - string 'method' the request method (GET, POST etc) - * - string|false 'requesttoken' the requesttoken or false when not available + * - array 'urlParams' the parameters which were matched from the URL + * - array 'get' the $_GET array + * - array|string 'post' the $_POST array or JSON string + * - array 'files' the $_FILES array + * - array 'server' the $_SERVER array + * - array 'env' the $_ENV array + * - array 'cookies' the $_COOKIE array + * - string 'method' the request method (GET, POST etc) + * - string|false 'requesttoken' the requesttoken or false when not available * @param IRequestId $requestId * @param IConfig $config * @param CsrfTokenManager|null $csrfTokenManager @@ -283,11 +286,11 @@ class Request implements \ArrayAccess, \Countable, IRequest { * In case of json requests the encoded json body is accessed * * @param string $key the key which you want to access in the URL Parameter - * placeholder, $_POST or $_GET array. - * The priority how they're returned is the following: - * 1. URL parameters - * 2. POST parameters - * 3. GET parameters + * placeholder, $_POST or $_GET array. + * The priority how they're returned is the following: + * 1. URL parameters + * 2. POST parameters + * 3. GET parameters * @param mixed $default If the key is not found, this value will be returned * @return mixed the content of the array */ @@ -355,13 +358,13 @@ class Request implements \ArrayAccess, \Countable, IRequest { protected function getContent() { // If the content can't be parsed into an array then return a stream resource. if ($this->isPutStreamContent()) { - if ($this->content === false) { + if ($this->isPutStreamContentAlreadySent) { throw new \LogicException( '"put" can only be accessed once if not ' . 'application/x-www-form-urlencoded or application/json.' ); } - $this->content = false; + $this->isPutStreamContentAlreadySent = true; return fopen($this->inputStream, 'rb'); } else { $this->decodeContent(); @@ -388,7 +391,14 @@ class Request implements \ArrayAccess, \Countable, IRequest { // 'application/json' and other JSON-related content types must be decoded manually. if (preg_match(self::JSON_CONTENT_TYPE_REGEX, $this->getHeader('Content-Type')) === 1) { - $params = json_decode(file_get_contents($this->inputStream), true); + $content = file_get_contents($this->inputStream); + if ($content !== '') { + try { + $params = json_decode($content, true, flags:JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + $this->decodingException = $e; + } + } if (\is_array($params) && \count($params) > 0) { $this->items['params'] = $params; if ($this->method === 'POST') { @@ -412,6 +422,12 @@ class Request implements \ArrayAccess, \Countable, IRequest { $this->contentDecoded = true; } + public function throwDecodingExceptionIfAny(): void { + if ($this->decodingException !== null) { + throw $this->decodingException; + } + } + /** * Checks if the CSRF check was correct @@ -426,6 +442,10 @@ class Request implements \ArrayAccess, \Countable, IRequest { return false; } + if ($this->getHeader('OCS-APIRequest') !== '') { + return true; + } + if (isset($this->items['get']['requesttoken'])) { $token = $this->items['get']['requesttoken']; } elseif (isset($this->items['post']['requesttoken'])) { @@ -479,7 +499,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { $prefix = '__Host-'; } - return $prefix.$name; + return $prefix . $name; } /** @@ -601,43 +621,53 @@ class Request implements \ArrayAccess, \Countable, IRequest { * @return bool */ private function isOverwriteCondition(): bool { - $regex = '/' . $this->config->getSystemValueString('overwritecondaddr', '') . '/'; + $regex = '/' . $this->config->getSystemValueString('overwritecondaddr', '') . '/'; $remoteAddr = isset($this->server['REMOTE_ADDR']) ? $this->server['REMOTE_ADDR'] : ''; return $regex === '//' || preg_match($regex, $remoteAddr) === 1; } /** * Returns the server protocol. It respects one or more reverse proxies servers - * and load balancers + * and load balancers. Precedence: + * 1. `overwriteprotocol` config value + * 2. `X-Forwarded-Proto` header value + * 3. $_SERVER['HTTPS'] value + * If an invalid protocol is provided, defaults to http, continues, but logs as an error. + * * @return string Server protocol (http or https) */ public function getServerProtocol(): string { - if ($this->config->getSystemValueString('overwriteprotocol') !== '' - && $this->isOverwriteCondition()) { - return $this->config->getSystemValueString('overwriteprotocol'); - } + $proto = 'http'; - if ($this->fromTrustedProxy() && isset($this->server['HTTP_X_FORWARDED_PROTO'])) { + if ($this->config->getSystemValueString('overwriteprotocol') !== '' + && $this->isOverwriteCondition() + ) { + $proto = strtolower($this->config->getSystemValueString('overwriteprotocol')); + } elseif ($this->fromTrustedProxy() + && isset($this->server['HTTP_X_FORWARDED_PROTO']) + ) { if (str_contains($this->server['HTTP_X_FORWARDED_PROTO'], ',')) { $parts = explode(',', $this->server['HTTP_X_FORWARDED_PROTO']); $proto = strtolower(trim($parts[0])); } else { $proto = strtolower($this->server['HTTP_X_FORWARDED_PROTO']); } - - // Verify that the protocol is always HTTP or HTTPS - // default to http if an invalid value is provided - return $proto === 'https' ? 'https' : 'http'; + } elseif (!empty($this->server['HTTPS']) + && $this->server['HTTPS'] !== 'off' + ) { + $proto = 'https'; } - if (isset($this->server['HTTPS']) - && $this->server['HTTPS'] !== null - && $this->server['HTTPS'] !== 'off' - && $this->server['HTTPS'] !== '') { - return 'https'; + if ($proto !== 'https' && $proto !== 'http') { + // log unrecognized value so admin has a chance to fix it + \OCP\Server::get(LoggerInterface::class)->critical( + 'Server protocol is malformed [falling back to http] (check overwriteprotocol and/or X-Forwarded-Proto to remedy): ' . $proto, + ['app' => 'core'] + ); } - return 'http'; + // default to http if provided an invalid value + return $proto === 'https' ? 'https' : 'http'; } /** @@ -724,11 +754,11 @@ class Request implements \ArrayAccess, \Countable, IRequest { } /** - * Get PathInfo from request + * Get PathInfo from request (rawurldecoded) * @throws \Exception * @return string|false Path info or false when not found */ - public function getPathInfo() { + public function getPathInfo(): string|false { $pathInfo = $this->getRawPathInfo(); return \Sabre\HTTP\decodePath($pathInfo); } @@ -832,7 +862,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { * Returns the overwritehost setting from the config if set and * if the overwrite condition is met * @return string|null overwritehost value or null if not defined or the defined condition - * isn't met + * isn't met */ private function getOverwriteHost() { if ($this->config->getSystemValueString('overwritehost') !== '' && $this->isOverwriteCondition()) { diff --git a/lib/private/AppFramework/Logger.php b/lib/private/AppFramework/Logger.php deleted file mode 100644 index 4ae4e6cae34..00000000000 --- a/lib/private/AppFramework/Logger.php +++ /dev/null @@ -1,108 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -namespace OC\AppFramework; - -use OCP\ILogger; - -/** - * @deprecated - */ -class Logger implements ILogger { - /** @var ILogger */ - private $logger; - - /** @var string */ - private $appName; - - /** - * @deprecated - */ - public function __construct(ILogger $logger, string $appName) { - $this->logger = $logger; - $this->appName = $appName; - } - - private function extendContext(array $context): array { - if (!isset($context['app'])) { - $context['app'] = $this->appName; - } - - return $context; - } - - /** - * @deprecated - */ - public function emergency(string $message, array $context = []) { - $this->logger->emergency($message, $this->extendContext($context)); - } - - /** - * @deprecated - */ - public function alert(string $message, array $context = []) { - $this->logger->alert($message, $this->extendContext($context)); - } - - /** - * @deprecated - */ - public function critical(string $message, array $context = []) { - $this->logger->critical($message, $this->extendContext($context)); - } - - /** - * @deprecated - */ - public function error(string $message, array $context = []) { - $this->logger->emergency($message, $this->extendContext($context)); - } - - /** - * @deprecated - */ - public function warning(string $message, array $context = []) { - $this->logger->warning($message, $this->extendContext($context)); - } - - /** - * @deprecated - */ - public function notice(string $message, array $context = []) { - $this->logger->notice($message, $this->extendContext($context)); - } - - /** - * @deprecated - */ - public function info(string $message, array $context = []) { - $this->logger->info($message, $this->extendContext($context)); - } - - /** - * @deprecated - */ - public function debug(string $message, array $context = []) { - $this->logger->debug($message, $this->extendContext($context)); - } - - /** - * @deprecated - */ - public function log(int $level, string $message, array $context = []) { - $this->logger->log($level, $message, $this->extendContext($context)); - } - - /** - * @deprecated - */ - public function logException(\Throwable $exception, array $context = []) { - $this->logger->logException($exception, $this->extendContext($context)); - } -} diff --git a/lib/private/AppFramework/Middleware/FlowV2EphemeralSessionsMiddleware.php b/lib/private/AppFramework/Middleware/FlowV2EphemeralSessionsMiddleware.php new file mode 100644 index 00000000000..b69b129f798 --- /dev/null +++ b/lib/private/AppFramework/Middleware/FlowV2EphemeralSessionsMiddleware.php @@ -0,0 +1,64 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\AppFramework\Middleware; + +use OC\AppFramework\Utility\ControllerMethodReflector; +use OC\Core\Controller\ClientFlowLoginV2Controller; +use OC\Core\Controller\TwoFactorChallengeController; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\PublicPage; +use OCP\AppFramework\Middleware; +use OCP\ISession; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; +use ReflectionMethod; + +// Will close the session if the user session is ephemeral. +// Happens when the user logs in via the login flow v2. +class FlowV2EphemeralSessionsMiddleware extends Middleware { + public function __construct( + private ISession $session, + private IUserSession $userSession, + private ControllerMethodReflector $reflector, + private LoggerInterface $logger, + ) { + } + + public function beforeController(Controller $controller, string $methodName) { + if (!$this->session->get(ClientFlowLoginV2Controller::EPHEMERAL_NAME)) { + return; + } + + if ( + $controller instanceof ClientFlowLoginV2Controller + && ($methodName === 'grantPage' || $methodName === 'generateAppPassword') + ) { + return; + } + + if ($controller instanceof TwoFactorChallengeController) { + return; + } + + $reflectionMethod = new ReflectionMethod($controller, $methodName); + if (!empty($reflectionMethod->getAttributes(PublicPage::class))) { + return; + } + + if ($this->reflector->hasAnnotation('PublicPage')) { + return; + } + + $this->logger->info('Closing user and PHP session for ephemeral session', [ + 'controller' => $controller::class, + 'method' => $methodName, + ]); + $this->userSession->logout(); + $this->session->close(); + } +} diff --git a/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php b/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php index 2b5acc8b75f..c9b51f26f34 100644 --- a/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php +++ b/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php @@ -23,7 +23,7 @@ class MiddlewareDispatcher { /** * @var int counter which tells us what middleware was executed once an - * exception occurs + * exception occurs */ private int $middlewareCounter; @@ -84,10 +84,10 @@ class MiddlewareDispatcher { * * @param Controller $controller the controller that is being called * @param string $methodName the name of the method that will be called on - * the controller + * the controller * @param \Exception $exception the thrown exception * @return Response a Response object if the middleware can handle the - * exception + * exception * @throws \Exception the passed in exception if it can't handle it */ public function afterException(Controller $controller, string $methodName, \Exception $exception): Response { @@ -109,7 +109,7 @@ class MiddlewareDispatcher { * * @param Controller $controller the controller that is being called * @param string $methodName the name of the method that will be called on - * the controller + * the controller * @param Response $response the generated response from the controller * @return Response a Response object */ diff --git a/lib/private/AppFramework/Middleware/NotModifiedMiddleware.php b/lib/private/AppFramework/Middleware/NotModifiedMiddleware.php index 17b423164f6..08b30092155 100644 --- a/lib/private/AppFramework/Middleware/NotModifiedMiddleware.php +++ b/lib/private/AppFramework/Middleware/NotModifiedMiddleware.php @@ -29,7 +29,7 @@ class NotModifiedMiddleware extends Middleware { } $modifiedSinceHeader = $this->request->getHeader('IF_MODIFIED_SINCE'); - if ($modifiedSinceHeader !== '' && $response->getLastModified() !== null && trim($modifiedSinceHeader) === $response->getLastModified()->format(\DateTimeInterface::RFC2822)) { + if ($modifiedSinceHeader !== '' && $response->getLastModified() !== null && trim($modifiedSinceHeader) === $response->getLastModified()->format(\DateTimeInterface::RFC7231)) { $response->setStatus(Http::STATUS_NOT_MODIFIED); return $response; } diff --git a/lib/private/AppFramework/Middleware/OCSMiddleware.php b/lib/private/AppFramework/Middleware/OCSMiddleware.php index 46612bf0d29..64f4b0054de 100644 --- a/lib/private/AppFramework/Middleware/OCSMiddleware.php +++ b/lib/private/AppFramework/Middleware/OCSMiddleware.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/AppFramework/Middleware/PublicShare/Exceptions/NeedAuthenticationException.php b/lib/private/AppFramework/Middleware/PublicShare/Exceptions/NeedAuthenticationException.php index c80d06c90ae..5df4009b094 100644 --- a/lib/private/AppFramework/Middleware/PublicShare/Exceptions/NeedAuthenticationException.php +++ b/lib/private/AppFramework/Middleware/PublicShare/Exceptions/NeedAuthenticationException.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/AppFramework/Middleware/PublicShare/PublicShareMiddleware.php b/lib/private/AppFramework/Middleware/PublicShare/PublicShareMiddleware.php index 41cba1aacd3..83e799e3d3b 100644 --- a/lib/private/AppFramework/Middleware/PublicShare/PublicShareMiddleware.php +++ b/lib/private/AppFramework/Middleware/PublicShare/PublicShareMiddleware.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -6,8 +7,10 @@ namespace OC\AppFramework\Middleware\PublicShare; use OC\AppFramework\Middleware\PublicShare\Exceptions\NeedAuthenticationException; +use OCA\Files_Sharing\AppInfo\Application; use OCP\AppFramework\AuthPublicShareController; -use OCP\AppFramework\Http\NotFoundResponse; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Middleware; use OCP\AppFramework\PublicShareController; use OCP\Files\NotFoundException; @@ -17,23 +20,13 @@ use OCP\ISession; use OCP\Security\Bruteforce\IThrottler; class PublicShareMiddleware extends Middleware { - /** @var IRequest */ - private $request; - - /** @var ISession */ - private $session; - - /** @var IConfig */ - private $config; - - /** @var IThrottler */ - private $throttler; - public function __construct(IRequest $request, ISession $session, IConfig $config, IThrottler $throttler) { - $this->request = $request; - $this->session = $session; - $this->config = $config; - $this->throttler = $throttler; + public function __construct( + private IRequest $request, + private ISession $session, + private IConfig $config, + private IThrottler $throttler, + ) { } public function beforeController($controller, $methodName) { @@ -92,7 +85,9 @@ class PublicShareMiddleware extends Middleware { } if ($exception instanceof NotFoundException) { - return new NotFoundResponse(); + return new TemplateResponse(Application::APP_ID, 'sharenotfound', [ + 'message' => $exception->getMessage(), + ], 'guest', Http::STATUS_NOT_FOUND); } if ($controller instanceof AuthPublicShareController && $exception instanceof NeedAuthenticationException) { @@ -126,7 +121,7 @@ class PublicShareMiddleware extends Middleware { private function throttle($bruteforceProtectionAction, $token): void { $ip = $this->request->getRemoteAddress(); - $this->throttler->sleepDelay($ip, $bruteforceProtectionAction); + $this->throttler->sleepDelayOrThrowOnMax($ip, $bruteforceProtectionAction); $this->throttler->registerAttempt($bruteforceProtectionAction, $ip, ['token' => $token]); } } diff --git a/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php index 7b617b22e3c..4453f5a7d4b 100644 --- a/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php @@ -21,6 +21,7 @@ use OCP\AppFramework\Middleware; use OCP\IRequest; use OCP\ISession; use OCP\Security\Bruteforce\IThrottler; +use Psr\Log\LoggerInterface; use ReflectionMethod; /** @@ -30,7 +31,7 @@ use ReflectionMethod; * https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS */ class CORSMiddleware extends Middleware { - /** @var IRequest */ + /** @var IRequest */ private $request; /** @var ControllerMethodReflector */ private $reflector; @@ -39,10 +40,13 @@ class CORSMiddleware extends Middleware { /** @var IThrottler */ private $throttler; - public function __construct(IRequest $request, + public function __construct( + IRequest $request, ControllerMethodReflector $reflector, Session $session, - IThrottler $throttler) { + IThrottler $throttler, + private readonly LoggerInterface $logger, + ) { $this->request = $request; $this->reflector = $reflector; $this->session = $session; @@ -64,8 +68,8 @@ class CORSMiddleware extends Middleware { // ensure that @CORS annotated API routes are not used in conjunction // with session authentication since this enables CSRF attack vectors - if ($this->hasAnnotationOrAttribute($reflectionMethod, 'CORS', CORS::class) && - (!$this->hasAnnotationOrAttribute($reflectionMethod, 'PublicPage', PublicPage::class) || $this->session->isLoggedIn())) { + if ($this->hasAnnotationOrAttribute($reflectionMethod, 'CORS', CORS::class) + && (!$this->hasAnnotationOrAttribute($reflectionMethod, 'PublicPage', PublicPage::class) || $this->session->isLoggedIn())) { $user = array_key_exists('PHP_AUTH_USER', $this->request->server) ? $this->request->server['PHP_AUTH_USER'] : null; $pass = array_key_exists('PHP_AUTH_PW', $this->request->server) ? $this->request->server['PHP_AUTH_PW'] : null; @@ -98,6 +102,7 @@ class CORSMiddleware extends Middleware { */ protected function hasAnnotationOrAttribute(ReflectionMethod $reflectionMethod, string $annotationName, string $attributeClass): bool { if ($this->reflector->hasAnnotation($annotationName)) { + $this->logger->debug($reflectionMethod->getDeclaringClass()->getName() . '::' . $reflectionMethod->getName() . ' uses the @' . $annotationName . ' annotation and should use the #[' . $attributeClass . '] attribute instead'); return true; } @@ -129,10 +134,10 @@ class CORSMiddleware extends Middleware { // allow credentials headers must not be true or CSRF is possible // otherwise foreach ($response->getHeaders() as $header => $value) { - if (strtolower($header) === 'access-control-allow-credentials' && - strtolower(trim($value)) === 'true') { - $msg = 'Access-Control-Allow-Credentials must not be '. - 'set to true in order to prevent CSRF'; + if (strtolower($header) === 'access-control-allow-credentials' + && strtolower(trim($value)) === 'true') { + $msg = 'Access-Control-Allow-Credentials must not be ' + . 'set to true in order to prevent CSRF'; throw new SecurityException($msg); } } diff --git a/lib/private/AppFramework/Middleware/Security/CSPMiddleware.php b/lib/private/AppFramework/Middleware/Security/CSPMiddleware.php index 2115c07c0fc..e88c9563c00 100644 --- a/lib/private/AppFramework/Middleware/Security/CSPMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/CSPMiddleware.php @@ -10,7 +10,6 @@ namespace OC\AppFramework\Middleware\Security; use OC\Security\CSP\ContentSecurityPolicyManager; use OC\Security\CSP\ContentSecurityPolicyNonceManager; -use OC\Security\CSRF\CsrfTokenManager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\ContentSecurityPolicy; use OCP\AppFramework\Http\EmptyContentSecurityPolicy; @@ -18,19 +17,11 @@ use OCP\AppFramework\Http\Response; use OCP\AppFramework\Middleware; class CSPMiddleware extends Middleware { - /** @var ContentSecurityPolicyManager */ - private $contentSecurityPolicyManager; - /** @var ContentSecurityPolicyNonceManager */ - private $cspNonceManager; - /** @var CsrfTokenManager */ - private $csrfTokenManager; - - public function __construct(ContentSecurityPolicyManager $policyManager, - ContentSecurityPolicyNonceManager $cspNonceManager, - CsrfTokenManager $csrfTokenManager) { - $this->contentSecurityPolicyManager = $policyManager; - $this->cspNonceManager = $cspNonceManager; - $this->csrfTokenManager = $csrfTokenManager; + + public function __construct( + private ContentSecurityPolicyManager $policyManager, + private ContentSecurityPolicyNonceManager $cspNonceManager, + ) { } /** @@ -49,11 +40,11 @@ class CSPMiddleware extends Middleware { return $response; } - $defaultPolicy = $this->contentSecurityPolicyManager->getDefaultPolicy(); - $defaultPolicy = $this->contentSecurityPolicyManager->mergePolicies($defaultPolicy, $policy); + $defaultPolicy = $this->policyManager->getDefaultPolicy(); + $defaultPolicy = $this->policyManager->mergePolicies($defaultPolicy, $policy); if ($this->cspNonceManager->browserSupportsCspV3()) { - $defaultPolicy->useJsNonce($this->csrfTokenManager->getToken()->getEncryptedValue()); + $defaultPolicy->useJsNonce($this->cspNonceManager->getNonce()); } $response->setContentSecurityPolicy($defaultPolicy); diff --git a/lib/private/AppFramework/Middleware/Security/Exceptions/AdminIpNotAllowedException.php b/lib/private/AppFramework/Middleware/Security/Exceptions/AdminIpNotAllowedException.php new file mode 100644 index 00000000000..36eb8f18928 --- /dev/null +++ b/lib/private/AppFramework/Middleware/Security/Exceptions/AdminIpNotAllowedException.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\AppFramework\Middleware\Security\Exceptions; + +use OCP\AppFramework\Http; + +/** + * Class AdminIpNotAllowed is thrown when a resource has been requested by a + * an admin user connecting from an unauthorized IP address + * See configuration `allowed_admin_ranges` + * + * @package OC\AppFramework\Middleware\Security\Exceptions + */ +class AdminIpNotAllowedException extends SecurityException { + public function __construct(string $message) { + parent::__construct($message, Http::STATUS_FORBIDDEN); + } +} diff --git a/lib/private/AppFramework/Middleware/Security/Exceptions/AppNotEnabledException.php b/lib/private/AppFramework/Middleware/Security/Exceptions/AppNotEnabledException.php index 646a5240bfc..53fbaaf5ed2 100644 --- a/lib/private/AppFramework/Middleware/Security/Exceptions/AppNotEnabledException.php +++ b/lib/private/AppFramework/Middleware/Security/Exceptions/AppNotEnabledException.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/lib/private/AppFramework/Middleware/Security/Exceptions/LaxSameSiteCookieFailedException.php b/lib/private/AppFramework/Middleware/Security/Exceptions/LaxSameSiteCookieFailedException.php index 91f1dba874d..0380c6781aa 100644 --- a/lib/private/AppFramework/Middleware/Security/Exceptions/LaxSameSiteCookieFailedException.php +++ b/lib/private/AppFramework/Middleware/Security/Exceptions/LaxSameSiteCookieFailedException.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/AppFramework/Middleware/Security/Exceptions/NotConfirmedException.php b/lib/private/AppFramework/Middleware/Security/Exceptions/NotConfirmedException.php index 7e950f2c976..ca30f736fbc 100644 --- a/lib/private/AppFramework/Middleware/Security/Exceptions/NotConfirmedException.php +++ b/lib/private/AppFramework/Middleware/Security/Exceptions/NotConfirmedException.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -14,7 +15,7 @@ use OCP\AppFramework\Http; * @package OC\AppFramework\Middleware\Security\Exceptions */ class NotConfirmedException extends SecurityException { - public function __construct() { - parent::__construct('Password confirmation is required', Http::STATUS_FORBIDDEN); + public function __construct(string $message = 'Password confirmation is required') { + parent::__construct($message, Http::STATUS_FORBIDDEN); } } diff --git a/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php b/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php index 5ff9d7386da..0facbffe504 100644 --- a/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -8,6 +9,7 @@ namespace OC\AppFramework\Middleware\Security; use OC\AppFramework\Middleware\Security\Exceptions\NotConfirmedException; use OC\AppFramework\Utility\ControllerMethodReflector; use OC\Authentication\Token\IProvider; +use OC\User\Manager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; use OCP\AppFramework\Middleware; @@ -16,82 +18,81 @@ use OCP\Authentication\Exceptions\ExpiredTokenException; use OCP\Authentication\Exceptions\InvalidTokenException; use OCP\Authentication\Exceptions\WipeTokenException; use OCP\Authentication\Token\IToken; +use OCP\IRequest; use OCP\ISession; use OCP\IUserSession; use OCP\Session\Exceptions\SessionNotAvailableException; use OCP\User\Backend\IPasswordConfirmationBackend; +use Psr\Log\LoggerInterface; use ReflectionMethod; class PasswordConfirmationMiddleware extends Middleware { - /** @var ControllerMethodReflector */ - private $reflector; - /** @var ISession */ - private $session; - /** @var IUserSession */ - private $userSession; - /** @var ITimeFactory */ - private $timeFactory; - /** @var array */ - private $excludedUserBackEnds = ['user_saml' => true, 'user_globalsiteselector' => true]; - private IProvider $tokenProvider; + private array $excludedUserBackEnds = ['user_saml' => true, 'user_globalsiteselector' => true]; - /** - * PasswordConfirmationMiddleware constructor. - * - * @param ControllerMethodReflector $reflector - * @param ISession $session - * @param IUserSession $userSession - * @param ITimeFactory $timeFactory - */ - public function __construct(ControllerMethodReflector $reflector, - ISession $session, - IUserSession $userSession, - ITimeFactory $timeFactory, - IProvider $tokenProvider, + public function __construct( + private ControllerMethodReflector $reflector, + private ISession $session, + private IUserSession $userSession, + private ITimeFactory $timeFactory, + private IProvider $tokenProvider, + private readonly LoggerInterface $logger, + private readonly IRequest $request, + private readonly Manager $userManager, ) { - $this->reflector = $reflector; - $this->session = $session; - $this->userSession = $userSession; - $this->timeFactory = $timeFactory; - $this->tokenProvider = $tokenProvider; } /** - * @param Controller $controller - * @param string $methodName * @throws NotConfirmedException */ - public function beforeController($controller, $methodName) { + public function beforeController(Controller $controller, string $methodName) { $reflectionMethod = new ReflectionMethod($controller, $methodName); - if ($this->hasAnnotationOrAttribute($reflectionMethod, 'PasswordConfirmationRequired', PasswordConfirmationRequired::class)) { - $user = $this->userSession->getUser(); - $backendClassName = ''; - if ($user !== null) { - $backend = $user->getBackend(); - if ($backend instanceof IPasswordConfirmationBackend) { - if (!$backend->canConfirmPassword($user->getUID())) { - return; - } - } + if (!$this->needsPasswordConfirmation($reflectionMethod)) { + return; + } - $backendClassName = $user->getBackendClassName(); + $user = $this->userSession->getUser(); + $backendClassName = ''; + if ($user !== null) { + $backend = $user->getBackend(); + if ($backend instanceof IPasswordConfirmationBackend) { + if (!$backend->canConfirmPassword($user->getUID())) { + return; + } } - try { - $sessionId = $this->session->getId(); - $token = $this->tokenProvider->getToken($sessionId); - } catch (SessionNotAvailableException|InvalidTokenException|WipeTokenException|ExpiredTokenException) { - // States we do not deal with here. - return; + $backendClassName = $user->getBackendClassName(); + } + + try { + $sessionId = $this->session->getId(); + $token = $this->tokenProvider->getToken($sessionId); + } catch (SessionNotAvailableException|InvalidTokenException|WipeTokenException|ExpiredTokenException) { + // States we do not deal with here. + return; + } + + $scope = $token->getScopeAsArray(); + if (isset($scope[IToken::SCOPE_SKIP_PASSWORD_VALIDATION]) && $scope[IToken::SCOPE_SKIP_PASSWORD_VALIDATION] === true) { + // Users logging in from SSO backends cannot confirm their password by design + return; + } + + if ($this->isPasswordConfirmationStrict($reflectionMethod)) { + $authHeader = $this->request->getHeader('Authorization'); + if (!str_starts_with(strtolower($authHeader), 'basic ')) { + throw new NotConfirmedException('Required authorization header missing'); } - $scope = $token->getScopeAsArray(); - if (isset($scope[IToken::SCOPE_SKIP_PASSWORD_VALIDATION]) && $scope[IToken::SCOPE_SKIP_PASSWORD_VALIDATION] === true) { - // Users logging in from SSO backends cannot confirm their password by design - return; + [, $password] = explode(':', base64_decode(substr($authHeader, 6)), 2); + $loginName = $this->session->get('loginname'); + $loginResult = $this->userManager->checkPassword($loginName, $password); + if ($loginResult === false) { + throw new NotConfirmedException(); } - $lastConfirm = (int) $this->session->get('last-password-confirm'); + $this->session->set('last-password-confirm', $this->timeFactory->getTime()); + } else { + $lastConfirm = (int)$this->session->get('last-password-confirm'); // TODO: confirm excludedUserBackEnds can go away and remove it if (!isset($this->excludedUserBackEnds[$backendClassName]) && $lastConfirm < ($this->timeFactory->getTime() - (30 * 60 + 15))) { // allow 15 seconds delay throw new NotConfirmedException(); @@ -99,23 +100,22 @@ class PasswordConfirmationMiddleware extends Middleware { } } - /** - * @template T - * - * @param ReflectionMethod $reflectionMethod - * @param string $annotationName - * @param class-string<T> $attributeClass - * @return boolean - */ - protected function hasAnnotationOrAttribute(ReflectionMethod $reflectionMethod, string $annotationName, string $attributeClass): bool { - if (!empty($reflectionMethod->getAttributes($attributeClass))) { + private function needsPasswordConfirmation(ReflectionMethod $reflectionMethod): bool { + $attributes = $reflectionMethod->getAttributes(PasswordConfirmationRequired::class); + if (!empty($attributes)) { return true; } - if ($this->reflector->hasAnnotation($annotationName)) { + if ($this->reflector->hasAnnotation('PasswordConfirmationRequired')) { + $this->logger->debug($reflectionMethod->getDeclaringClass()->getName() . '::' . $reflectionMethod->getName() . ' uses the @' . 'PasswordConfirmationRequired' . ' annotation and should use the #[PasswordConfirmationRequired] attribute instead'); return true; } return false; } + + private function isPasswordConfirmationStrict(ReflectionMethod $reflectionMethod): bool { + $attributes = $reflectionMethod->getAttributes(PasswordConfirmationRequired::class); + return !empty($attributes) && ($attributes[0]->newInstance()->getStrict()); + } } diff --git a/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php b/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php index d593bf5019f..2d19be97993 100644 --- a/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php @@ -9,8 +9,10 @@ declare(strict_types=1); namespace OC\AppFramework\Middleware\Security; use OC\AppFramework\Utility\ControllerMethodReflector; +use OC\Security\Ip\BruteforceAllowList; use OC\Security\RateLimiting\Exception\RateLimitExceededException; use OC\Security\RateLimiting\Limiter; +use OC\User\Session; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\Attribute\AnonRateLimit; use OCP\AppFramework\Http\Attribute\ARateLimit; @@ -19,6 +21,7 @@ use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Middleware; +use OCP\IAppConfig; use OCP\IRequest; use OCP\ISession; use OCP\IUserSession; @@ -52,6 +55,8 @@ class RateLimitingMiddleware extends Middleware { protected ControllerMethodReflector $reflector, protected Limiter $limiter, protected ISession $session, + protected IAppConfig $appConfig, + protected BruteforceAllowList $bruteForceAllowList, ) { } @@ -63,8 +68,8 @@ class RateLimitingMiddleware extends Middleware { parent::beforeController($controller, $methodName); $rateLimitIdentifier = get_class($controller) . '::' . $methodName; - if ($this->session->exists('app_api_system')) { - // Bypass rate limiting for app_api + if ($this->userSession instanceof Session && $this->userSession->getSession()->get('app_api') === true && $this->userSession->getUser() === null) { + // if userId is not specified and the request is authenticated by AppAPI, we skip the rate limit return; } @@ -72,6 +77,11 @@ class RateLimitingMiddleware extends Middleware { $rateLimit = $this->readLimitFromAnnotationOrAttribute($controller, $methodName, 'UserRateThrottle', UserRateLimit::class); if ($rateLimit !== null) { + if ($this->appConfig->getValueBool('bruteforcesettings', 'apply_allowlist_to_ratelimit') + && $this->bruteForceAllowList->isBypassListed($this->request->getRemoteAddress())) { + return; + } + $this->limiter->registerUserRequest( $rateLimitIdentifier, $rateLimit->getLimit(), @@ -111,8 +121,8 @@ class RateLimitingMiddleware extends Middleware { if ($annotationLimit !== '' && $annotationPeriod !== '') { return new $attributeClass( - (int) $annotationLimit, - (int) $annotationPeriod, + (int)$annotationLimit, + (int)$annotationPeriod, ); } diff --git a/lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php b/lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php index e0bb96f132b..097ed1b2b8f 100644 --- a/lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -13,16 +14,10 @@ use OCP\AppFramework\Http\Response; use OCP\AppFramework\Middleware; class SameSiteCookieMiddleware extends Middleware { - /** @var Request */ - private $request; - - /** @var ControllerMethodReflector */ - private $reflector; - - public function __construct(Request $request, - ControllerMethodReflector $reflector) { - $this->request = $request; - $this->reflector = $reflector; + public function __construct( + private Request $request, + private ControllerMethodReflector $reflector, + ) { } public function beforeController($controller, $methodName) { @@ -46,19 +41,19 @@ class SameSiteCookieMiddleware extends Middleware { public function afterException($controller, $methodName, \Exception $exception) { if ($exception instanceof LaxSameSiteCookieFailedException) { - $respone = new Response(); - $respone->setStatus(Http::STATUS_FOUND); - $respone->addHeader('Location', $this->request->getRequestUri()); + $response = new Response(); + $response->setStatus(Http::STATUS_FOUND); + $response->addHeader('Location', $this->request->getRequestUri()); $this->setSameSiteCookie(); - return $respone; + return $response; } throw $exception; } - protected function setSameSiteCookie() { + protected function setSameSiteCookie(): void { $cookieParams = $this->request->getCookieParams(); $secureCookie = ($cookieParams['secure'] === true) ? 'secure; ' : ''; $policies = [ diff --git a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php index bc2014da246..e3a293e0fd9 100644 --- a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php @@ -8,6 +8,7 @@ declare(strict_types=1); */ namespace OC\AppFramework\Middleware\Security; +use OC\AppFramework\Middleware\Security\Exceptions\AdminIpNotAllowedException; use OC\AppFramework\Middleware\Security\Exceptions\AppNotEnabledException; use OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException; use OC\AppFramework\Middleware\Security\Exceptions\ExAppRequiredException; @@ -21,6 +22,7 @@ use OC\User\Session; use OCP\App\AppPathNotFoundException; use OCP\App\IAppManager; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\AppApiAdminAccessWithoutUser; use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting; use OCP\AppFramework\Http\Attribute\ExAppRequired; use OCP\AppFramework\Http\Attribute\NoAdminRequired; @@ -34,11 +36,14 @@ use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Middleware; use OCP\AppFramework\OCSController; +use OCP\Group\ISubAdmin; +use OCP\IGroupManager; use OCP\IL10N; use OCP\INavigationManager; use OCP\IRequest; use OCP\IURLGenerator; use OCP\IUserSession; +use OCP\Security\Ip\IRemoteAddress; use OCP\Util; use Psr\Log\LoggerInterface; use ReflectionMethod; @@ -50,60 +55,41 @@ use ReflectionMethod; * check fails */ class SecurityMiddleware extends Middleware { - /** @var INavigationManager */ - private $navigationManager; - /** @var IRequest */ - private $request; - /** @var ControllerMethodReflector */ - private $reflector; - /** @var string */ - private $appName; - /** @var IURLGenerator */ - private $urlGenerator; - /** @var LoggerInterface */ - private $logger; - /** @var bool */ - private $isLoggedIn; - /** @var bool */ - private $isAdminUser; - /** @var bool */ - private $isSubAdmin; - /** @var IAppManager */ - private $appManager; - /** @var IL10N */ - private $l10n; - /** @var AuthorizedGroupMapper */ - private $groupAuthorizationMapper; - /** @var IUserSession */ - private $userSession; - - public function __construct(IRequest $request, - ControllerMethodReflector $reflector, - INavigationManager $navigationManager, - IURLGenerator $urlGenerator, - LoggerInterface $logger, - string $appName, - bool $isLoggedIn, - bool $isAdminUser, - bool $isSubAdmin, - IAppManager $appManager, - IL10N $l10n, - AuthorizedGroupMapper $mapper, - IUserSession $userSession + private ?bool $isAdminUser = null; + private ?bool $isSubAdmin = null; + + public function __construct( + private IRequest $request, + private ControllerMethodReflector $reflector, + private INavigationManager $navigationManager, + private IURLGenerator $urlGenerator, + private LoggerInterface $logger, + private string $appName, + private bool $isLoggedIn, + private IGroupManager $groupManager, + private ISubAdmin $subAdminManager, + private IAppManager $appManager, + private IL10N $l10n, + private AuthorizedGroupMapper $groupAuthorizationMapper, + private IUserSession $userSession, + private IRemoteAddress $remoteAddress, ) { - $this->navigationManager = $navigationManager; - $this->request = $request; - $this->reflector = $reflector; - $this->appName = $appName; - $this->urlGenerator = $urlGenerator; - $this->logger = $logger; - $this->isLoggedIn = $isLoggedIn; - $this->isAdminUser = $isAdminUser; - $this->isSubAdmin = $isSubAdmin; - $this->appManager = $appManager; - $this->l10n = $l10n; - $this->groupAuthorizationMapper = $mapper; - $this->userSession = $userSession; + } + + private function isAdminUser(): bool { + if ($this->isAdminUser === null) { + $user = $this->userSession->getUser(); + $this->isAdminUser = $user && $this->groupManager->isAdmin($user->getUID()); + } + return $this->isAdminUser; + } + + private function isSubAdmin(): bool { + if ($this->isSubAdmin === null) { + $user = $this->userSession->getUser(); + $this->isSubAdmin = $user && $this->subAdminManager->isSubAdmin($user); + } + return $this->isSubAdmin; } /** @@ -136,15 +122,23 @@ class SecurityMiddleware extends Middleware { throw new ExAppRequiredException(); } } elseif (!$isPublicPage) { - if (!$this->isLoggedIn) { + $authorized = false; + if ($this->hasAnnotationOrAttribute($reflectionMethod, null, AppApiAdminAccessWithoutUser::class)) { + // this attribute allows ExApp to access admin endpoints only if "userId" is "null" + if ($this->userSession instanceof Session && $this->userSession->getSession()->get('app_api') === true && $this->userSession->getUser() === null) { + $authorized = true; + } + } + + if (!$authorized && !$this->isLoggedIn) { throw new NotLoggedInException(); } - $authorized = false; - if ($this->hasAnnotationOrAttribute($reflectionMethod, 'AuthorizedAdminSetting', AuthorizedAdminSetting::class)) { - $authorized = $this->isAdminUser; + + if (!$authorized && $this->hasAnnotationOrAttribute($reflectionMethod, 'AuthorizedAdminSetting', AuthorizedAdminSetting::class)) { + $authorized = $this->isAdminUser(); if (!$authorized && $this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)) { - $authorized = $this->isSubAdmin; + $authorized = $this->isSubAdmin(); } if (!$authorized) { @@ -161,24 +155,37 @@ class SecurityMiddleware extends Middleware { if (!$authorized) { throw new NotAdminException($this->l10n->t('Logged in account must be an admin, a sub admin or gotten special right to access this setting')); } + if (!$this->remoteAddress->allowsAdminActions()) { + throw new AdminIpNotAllowedException($this->l10n->t('Your current IP address doesn\'t allow you to perform admin actions')); + } } if ($this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class) - && !$this->isSubAdmin - && !$this->isAdminUser + && !$this->isSubAdmin() + && !$this->isAdminUser() && !$authorized) { throw new NotAdminException($this->l10n->t('Logged in account must be an admin or sub admin')); } if (!$this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class) && !$this->hasAnnotationOrAttribute($reflectionMethod, 'NoAdminRequired', NoAdminRequired::class) - && !$this->isAdminUser + && !$this->isAdminUser() && !$authorized) { throw new NotAdminException($this->l10n->t('Logged in account must be an admin')); } + if ($this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class) + && !$this->remoteAddress->allowsAdminActions()) { + throw new AdminIpNotAllowedException($this->l10n->t('Your current IP address doesn\'t allow you to perform admin actions')); + } + if (!$this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class) + && !$this->hasAnnotationOrAttribute($reflectionMethod, 'NoAdminRequired', NoAdminRequired::class) + && !$this->remoteAddress->allowsAdminActions()) { + throw new AdminIpNotAllowedException($this->l10n->t('Your current IP address doesn\'t allow you to perform admin actions')); + } + } // Check for strict cookie requirement - if ($this->hasAnnotationOrAttribute($reflectionMethod, 'StrictCookieRequired', StrictCookiesRequired::class) || - !$this->hasAnnotationOrAttribute($reflectionMethod, 'NoCSRFRequired', NoCSRFRequired::class)) { + if ($this->hasAnnotationOrAttribute($reflectionMethod, 'StrictCookieRequired', StrictCookiesRequired::class) + || !$this->hasAnnotationOrAttribute($reflectionMethod, 'NoCSRFRequired', NoCSRFRequired::class)) { if (!$this->request->passesStrictCookieCheck()) { throw new StrictCookieMissingException(); } @@ -233,16 +240,17 @@ class SecurityMiddleware extends Middleware { * @template T * * @param ReflectionMethod $reflectionMethod - * @param string $annotationName + * @param ?string $annotationName * @param class-string<T> $attributeClass * @return boolean */ - protected function hasAnnotationOrAttribute(ReflectionMethod $reflectionMethod, string $annotationName, string $attributeClass): bool { + protected function hasAnnotationOrAttribute(ReflectionMethod $reflectionMethod, ?string $annotationName, string $attributeClass): bool { if (!empty($reflectionMethod->getAttributes($attributeClass))) { return true; } - if ($this->reflector->hasAnnotation($annotationName)) { + if ($annotationName && $this->reflector->hasAnnotation($annotationName)) { + $this->logger->debug($reflectionMethod->getDeclaringClass()->getName() . '::' . $reflectionMethod->getName() . ' uses the @' . $annotationName . ' annotation and should use the #[' . $attributeClass . '] attribute instead'); return true; } diff --git a/lib/private/AppFramework/OCS/BaseResponse.php b/lib/private/AppFramework/OCS/BaseResponse.php index 2e685de856b..05ce133db24 100644 --- a/lib/private/AppFramework/OCS/BaseResponse.php +++ b/lib/private/AppFramework/OCS/BaseResponse.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -11,10 +12,10 @@ use OCP\AppFramework\Http\Response; /** * @psalm-import-type DataResponseType from DataResponse - * @template S of int + * @template S of Http::STATUS_* * @template-covariant T of DataResponseType * @template H of array<string, mixed> - * @template-extends Response<int, array<string, mixed>> + * @template-extends Response<Http::STATUS_*, array<string, mixed>> */ abstract class BaseResponse extends Response { /** @var array */ @@ -83,9 +84,9 @@ abstract class BaseResponse extends Response { */ protected function renderResult(array $meta): string { $status = $this->getStatus(); - if ($status === Http::STATUS_NO_CONTENT || - $status === Http::STATUS_NOT_MODIFIED || - ($status >= 100 && $status <= 199)) { + if ($status === Http::STATUS_NO_CONTENT + || $status === Http::STATUS_NOT_MODIFIED + || ($status >= 100 && $status <= 199)) { // Those status codes are not supposed to have a body: // https://stackoverflow.com/q/8628725 return ''; @@ -99,7 +100,7 @@ abstract class BaseResponse extends Response { ]; if ($this->format === 'json') { - return json_encode($response, JSON_HEX_TAG); + return $this->toJson($response); } $writer = new \XMLWriter(); @@ -111,6 +112,14 @@ abstract class BaseResponse extends Response { return $writer->outputMemory(true); } + /** + * @psalm-taint-escape has_quotes + * @psalm-taint-escape html + */ + protected function toJson(array $array): string { + return \json_encode($array, \JSON_HEX_TAG); + } + protected function toXML(array $array, \XMLWriter $writer): void { foreach ($array as $k => $v) { if ($k === '@attributes' && is_array($v)) { @@ -133,7 +142,9 @@ abstract class BaseResponse extends Response { $v = []; } - if (\is_array($v)) { + if ($k === '$comment') { + $writer->writeComment($v); + } elseif (\is_array($v)) { $writer->startElement($k); $this->toXML($v, $writer); $writer->endElement(); @@ -141,8 +152,10 @@ abstract class BaseResponse extends Response { $writer->startElement($k); $this->toXML($v->jsonSerialize(), $writer); $writer->endElement(); + } elseif ($v === null) { + $writer->writeElement($k); } else { - $writer->writeElement($k, $v); + $writer->writeElement($k, (string)$v); } } } diff --git a/lib/private/AppFramework/OCS/V1Response.php b/lib/private/AppFramework/OCS/V1Response.php index c56aa9cf478..1c2c25f5cb0 100644 --- a/lib/private/AppFramework/OCS/V1Response.php +++ b/lib/private/AppFramework/OCS/V1Response.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -11,17 +12,17 @@ use OCP\AppFramework\OCSController; /** * @psalm-import-type DataResponseType from DataResponse - * @template S of int + * @template S of Http::STATUS_* * @template-covariant T of DataResponseType * @template H of array<string, mixed> - * @template-extends BaseResponse<int, DataResponseType, array<string, mixed>> + * @template-extends BaseResponse<Http::STATUS_*, DataResponseType, array<string, mixed>> */ class V1Response extends BaseResponse { /** * The V1 endpoint has very limited http status codes basically everything * is status 200 except 401 * - * @return int + * @return Http::STATUS_* */ public function getStatus() { $status = parent::getStatus(); diff --git a/lib/private/AppFramework/OCS/V2Response.php b/lib/private/AppFramework/OCS/V2Response.php index caa8302a673..efc9348eb37 100644 --- a/lib/private/AppFramework/OCS/V2Response.php +++ b/lib/private/AppFramework/OCS/V2Response.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -11,17 +12,17 @@ use OCP\AppFramework\OCSController; /** * @psalm-import-type DataResponseType from DataResponse - * @template S of int + * @template S of Http::STATUS_* * @template-covariant T of DataResponseType * @template H of array<string, mixed> - * @template-extends BaseResponse<int, DataResponseType, array<string, mixed>> + * @template-extends BaseResponse<Http::STATUS_*, DataResponseType, array<string, mixed>> */ class V2Response extends BaseResponse { /** * The V2 endpoint just passes on status codes. * Of course we have to map the OCS specific codes to proper HTTP status codes * - * @return int + * @return Http::STATUS_* */ public function getStatus() { $status = parent::getStatus(); diff --git a/lib/private/AppFramework/Routing/RouteConfig.php b/lib/private/AppFramework/Routing/RouteConfig.php deleted file mode 100644 index bdeae93c7c8..00000000000 --- a/lib/private/AppFramework/Routing/RouteConfig.php +++ /dev/null @@ -1,278 +0,0 @@ -<?php - -declare(strict_types=1); -/** - * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-FileCopyrightText: 2016 ownCloud, Inc. - * SPDX-License-Identifier: AGPL-3.0-only - */ -namespace OC\AppFramework\Routing; - -use OC\AppFramework\DependencyInjection\DIContainer; -use OC\Route\Router; - -/** - * Class RouteConfig - * @package OC\AppFramework\routing - */ -class RouteConfig { - /** @var DIContainer */ - private $container; - - /** @var Router */ - private $router; - - /** @var array */ - private $routes; - - /** @var string */ - private $appName; - - /** @var string[] */ - private $controllerNameCache = []; - - protected $rootUrlApps = [ - 'cloud_federation_api', - 'core', - 'files_sharing', - 'files', - 'settings', - 'spreed', - ]; - - /** - * @param \OC\AppFramework\DependencyInjection\DIContainer $container - * @param \OC\Route\Router $router - * @param array $routes - * @internal param $appName - */ - public function __construct(DIContainer $container, Router $router, $routes) { - $this->routes = $routes; - $this->container = $container; - $this->router = $router; - $this->appName = $container['AppName']; - } - - /** - * The routes and resource will be registered to the \OCP\Route\IRouter - */ - public function register() { - // parse simple - $this->processIndexRoutes($this->routes); - - // parse resources - $this->processIndexResources($this->routes); - - /* - * OCS routes go into a different collection - */ - $oldCollection = $this->router->getCurrentCollection(); - $this->router->useCollection($oldCollection . '.ocs'); - - // parse ocs simple routes - $this->processOCS($this->routes); - - // parse ocs simple routes - $this->processOCSResources($this->routes); - - $this->router->useCollection($oldCollection); - } - - private function processOCS(array $routes): void { - $ocsRoutes = $routes['ocs'] ?? []; - foreach ($ocsRoutes as $ocsRoute) { - $this->processRoute($ocsRoute, 'ocs.'); - } - } - - /** - * Creates one route base on the give configuration - * @param array $routes - * @throws \UnexpectedValueException - */ - private function processIndexRoutes(array $routes): void { - $simpleRoutes = $routes['routes'] ?? []; - foreach ($simpleRoutes as $simpleRoute) { - $this->processRoute($simpleRoute); - } - } - - protected function processRoute(array $route, string $routeNamePrefix = ''): void { - $name = $route['name']; - $postfix = $route['postfix'] ?? ''; - $root = $this->buildRootPrefix($route, $routeNamePrefix); - - $url = $root . '/' . ltrim($route['url'], '/'); - $verb = strtoupper($route['verb'] ?? 'GET'); - - $split = explode('#', $name, 2); - if (count($split) !== 2) { - throw new \UnexpectedValueException('Invalid route name: use the format foo#bar to reference FooController::bar'); - } - [$controller, $action] = $split; - - $controllerName = $this->buildControllerName($controller); - $actionName = $this->buildActionName($action); - - /* - * The route name has to be lowercase, for symfony to match it correctly. - * This is required because smyfony allows mixed casing for controller names in the routes. - * To avoid breaking all the existing route names, registering and matching will only use the lowercase names. - * This is also safe on the PHP side because class and method names collide regardless of the casing. - */ - $routeName = strtolower($routeNamePrefix . $this->appName . '.' . $controller . '.' . $action . $postfix); - - $router = $this->router->create($routeName, $url) - ->method($verb); - - // optionally register requirements for route. This is used to - // tell the route parser how url parameters should be matched - if (array_key_exists('requirements', $route)) { - $router->requirements($route['requirements']); - } - - // optionally register defaults for route. This is used to - // tell the route parser how url parameters should be default valued - $defaults = []; - if (array_key_exists('defaults', $route)) { - $defaults = $route['defaults']; - } - - $defaults['caller'] = [$this->appName, $controllerName, $actionName]; - $router->defaults($defaults); - } - - /** - * For a given name and url restful OCS routes are created: - * - index - * - show - * - create - * - update - * - destroy - * - * @param array $routes - */ - private function processOCSResources(array $routes): void { - $this->processResources($routes['ocs-resources'] ?? [], 'ocs.'); - } - - /** - * For a given name and url restful routes are created: - * - index - * - show - * - create - * - update - * - destroy - * - * @param array $routes - */ - private function processIndexResources(array $routes): void { - $this->processResources($routes['resources'] ?? []); - } - - /** - * For a given name and url restful routes are created: - * - index - * - show - * - create - * - update - * - destroy - * - * @param array $resources - * @param string $routeNamePrefix - */ - protected function processResources(array $resources, string $routeNamePrefix = ''): void { - // declaration of all restful actions - $actions = [ - ['name' => 'index', 'verb' => 'GET', 'on-collection' => true], - ['name' => 'show', 'verb' => 'GET'], - ['name' => 'create', 'verb' => 'POST', 'on-collection' => true], - ['name' => 'update', 'verb' => 'PUT'], - ['name' => 'destroy', 'verb' => 'DELETE'], - ]; - - foreach ($resources as $resource => $config) { - $root = $this->buildRootPrefix($config, $routeNamePrefix); - - // the url parameter used as id to the resource - foreach ($actions as $action) { - $url = $root . '/' . ltrim($config['url'], '/'); - $method = $action['name']; - - $verb = strtoupper($action['verb'] ?? 'GET'); - $collectionAction = $action['on-collection'] ?? false; - if (!$collectionAction) { - $url .= '/{id}'; - } - if (isset($action['url-postfix'])) { - $url .= '/' . $action['url-postfix']; - } - - $controller = $resource; - - $controllerName = $this->buildControllerName($controller); - $actionName = $this->buildActionName($method); - - $routeName = $routeNamePrefix . $this->appName . '.' . strtolower($resource) . '.' . $method; - - $route = $this->router->create($routeName, $url) - ->method($verb); - - $route->defaults(['caller' => [$this->appName, $controllerName, $actionName]]); - } - } - } - - private function buildRootPrefix(array $route, string $routeNamePrefix): string { - $defaultRoot = $this->appName === 'core' ? '' : '/apps/' . $this->appName; - $root = $route['root'] ?? $defaultRoot; - - if ($routeNamePrefix !== '') { - // In OCS all apps are whitelisted - return $root; - } - - if (!\in_array($this->appName, $this->rootUrlApps, true)) { - // Only allow root URLS for some apps - return $defaultRoot; - } - - return $root; - } - - /** - * Based on a given route name the controller name is generated - * @param string $controller - * @return string - */ - private function buildControllerName(string $controller): string { - if (!isset($this->controllerNameCache[$controller])) { - $this->controllerNameCache[$controller] = $this->underScoreToCamelCase(ucfirst($controller)) . 'Controller'; - } - return $this->controllerNameCache[$controller]; - } - - /** - * Based on the action part of the route name the controller method name is generated - * @param string $action - * @return string - */ - private function buildActionName(string $action): string { - return $this->underScoreToCamelCase($action); - } - - /** - * Underscored strings are converted to camel case strings - * @param string $str - * @return string - */ - private function underScoreToCamelCase(string $str): string { - $pattern = '/_[a-z]?/'; - return preg_replace_callback( - $pattern, - function ($matches) { - return strtoupper(ltrim($matches[0], '_')); - }, - $str); - } -} diff --git a/lib/private/AppFramework/Routing/RouteParser.php b/lib/private/AppFramework/Routing/RouteParser.php index 49a50852bf3..55e58234673 100644 --- a/lib/private/AppFramework/Routing/RouteParser.php +++ b/lib/private/AppFramework/Routing/RouteParser.php @@ -20,6 +20,7 @@ class RouteParser { 'core', 'files_sharing', 'files', + 'profile', 'settings', 'spreed', ]; @@ -75,7 +76,7 @@ class RouteParser { $url = $root . '/' . ltrim($route['url'], '/'); $verb = strtoupper($route['verb'] ?? 'GET'); - $split = explode('#', $name, 2); + $split = explode('#', $name, 3); if (count($split) !== 2) { throw new \UnexpectedValueException('Invalid route name: use the format foo#bar to reference FooController::bar'); } @@ -86,7 +87,7 @@ class RouteParser { /* * The route name has to be lowercase, for symfony to match it correctly. - * This is required because smyfony allows mixed casing for controller names in the routes. + * This is required because symfony allows mixed casing for controller names in the routes. * To avoid breaking all the existing route names, registering and matching will only use the lowercase names. * This is also safe on the PHP side because class and method names collide regardless of the casing. */ diff --git a/lib/private/AppFramework/Services/AppConfig.php b/lib/private/AppFramework/Services/AppConfig.php index e47bbc429d0..04d97738483 100644 --- a/lib/private/AppFramework/Services/AppConfig.php +++ b/lib/private/AppFramework/Services/AppConfig.php @@ -116,7 +116,7 @@ class AppConfig implements IAppConfig { string $key, string $value, bool $lazy = false, - bool $sensitive = false + bool $sensitive = false, ): bool { return $this->appConfig->setValueString($this->appName, $key, $value, $lazy, $sensitive); } @@ -138,7 +138,7 @@ class AppConfig implements IAppConfig { string $key, int $value, bool $lazy = false, - bool $sensitive = false + bool $sensitive = false, ): bool { return $this->appConfig->setValueInt($this->appName, $key, $value, $lazy, $sensitive); } @@ -160,7 +160,7 @@ class AppConfig implements IAppConfig { string $key, float $value, bool $lazy = false, - bool $sensitive = false + bool $sensitive = false, ): bool { return $this->appConfig->setValueFloat($this->appName, $key, $value, $lazy, $sensitive); } @@ -180,7 +180,7 @@ class AppConfig implements IAppConfig { public function setAppValueBool( string $key, bool $value, - bool $lazy = false + bool $lazy = false, ): bool { return $this->appConfig->setValueBool($this->appName, $key, $value, $lazy); } @@ -203,7 +203,7 @@ class AppConfig implements IAppConfig { string $key, array $value, bool $lazy = false, - bool $sensitive = false + bool $sensitive = false, ): bool { return $this->appConfig->setValueArray($this->appName, $key, $value, $lazy, $sensitive); } @@ -337,4 +337,13 @@ class AppConfig implements IAppConfig { public function deleteUserValue(string $userId, string $key): void { $this->config->deleteUserValue($userId, $this->appName, $key); } + + /** + * Returns the installed versions of all apps + * + * @return array<string, string> + */ + public function getAppInstalledVersions(bool $onlyEnabled = false): array { + return $this->appConfig->getAppInstalledVersions($onlyEnabled); + } } diff --git a/lib/private/AppFramework/Utility/ControllerMethodReflector.php b/lib/private/AppFramework/Utility/ControllerMethodReflector.php index 9c08f58b384..679e1788004 100644 --- a/lib/private/AppFramework/Utility/ControllerMethodReflector.php +++ b/lib/private/AppFramework/Utility/ControllerMethodReflector.php @@ -50,7 +50,7 @@ class ControllerMethodReflector implements IControllerMethodReflector { // extract type parameter information preg_match_all('/@param\h+(?P<type>\w+)\h+\$(?P<var>\w+)/', $docs, $matches); $this->types = array_combine($matches['var'], $matches['type']); - preg_match_all('/@psalm-param\h+(?P<type>\w+)<(?P<rangeMin>(-?\d+|min)),\h*(?P<rangeMax>(-?\d+|max))>\h+\$(?P<var>\w+)/', $docs, $matches); + preg_match_all('/@psalm-param\h+(\?)?(?P<type>\w+)<(?P<rangeMin>(-?\d+|min)),\h*(?P<rangeMax>(-?\d+|max))>(\|null)?\h+\$(?P<var>\w+)/', $docs, $matches); foreach ($matches['var'] as $index => $varName) { if ($matches['type'][$index] !== 'int') { // only int ranges are possible at the moment @@ -82,9 +82,9 @@ class ControllerMethodReflector implements IControllerMethodReflector { /** * Inspects the PHPDoc parameters for types * @param string $parameter the parameter whose type comments should be - * parsed + * parsed * @return string|null type in the type parameters (@param int $something) - * would return int or null if not existing + * would return int or null if not existing */ public function getType(string $parameter) { if (array_key_exists($parameter, $this->types)) { diff --git a/lib/private/AppFramework/Utility/SimpleContainer.php b/lib/private/AppFramework/Utility/SimpleContainer.php index bf0ef36d13c..0db3bfc1c77 100644 --- a/lib/private/AppFramework/Utility/SimpleContainer.php +++ b/lib/private/AppFramework/Utility/SimpleContainer.php @@ -12,7 +12,9 @@ use Closure; use OCP\AppFramework\QueryException; use OCP\IContainer; use Pimple\Container; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; use ReflectionClass; use ReflectionException; use ReflectionNamedType; @@ -23,8 +25,9 @@ use function class_exists; * SimpleContainer is a simple implementation of a container on basis of Pimple */ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer { - /** @var Container */ - private $container; + public static bool $useLazyObjects = false; + + private Container $container; public function __construct() { $this->container = new Container(); @@ -49,16 +52,29 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer { /** * @param ReflectionClass $class the class to instantiate - * @return \stdClass the created class + * @return object the created class * @suppress PhanUndeclaredClassInstanceof */ - private function buildClass(ReflectionClass $class) { + private function buildClass(ReflectionClass $class): object { $constructor = $class->getConstructor(); if ($constructor === null) { + /* No constructor, return a instance directly */ return $class->newInstance(); } + if (PHP_VERSION_ID >= 80400 && self::$useLazyObjects) { + /* For PHP>=8.4, use a lazy ghost to delay constructor and dependency resolving */ + /** @psalm-suppress UndefinedMethod */ + return $class->newLazyGhost(function (object $object) use ($constructor): void { + /** @psalm-suppress DirectConstructorCall For lazy ghosts we have to call the constructor directly */ + $object->__construct(...$this->buildClassConstructorParameters($constructor)); + }); + } else { + return $class->newInstanceArgs($this->buildClassConstructorParameters($constructor)); + } + } - return $class->newInstanceArgs(array_map(function (ReflectionParameter $parameter) { + private function buildClassConstructorParameters(\ReflectionMethod $constructor): array { + return array_map(function (ReflectionParameter $parameter) { $parameterType = $parameter->getType(); $resolveName = $parameter->getName(); @@ -69,10 +85,10 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer { } try { - $builtIn = $parameter->hasType() && ($parameter->getType() instanceof ReflectionNamedType) - && $parameter->getType()->isBuiltin(); + $builtIn = $parameterType !== null && ($parameterType instanceof ReflectionNamedType) + && $parameterType->isBuiltin(); return $this->query($resolveName, !$builtIn); - } catch (QueryException $e) { + } catch (ContainerExceptionInterface $e) { // Service not found, use the default value when available if ($parameter->isDefaultValueAvailable()) { return $parameter->getDefaultValue(); @@ -82,20 +98,20 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer { $resolveName = $parameter->getName(); try { return $this->query($resolveName); - } catch (QueryException $e2) { + } catch (ContainerExceptionInterface $e2) { // Pass null if typed and nullable if ($parameter->allowsNull() && ($parameterType instanceof ReflectionNamedType)) { return null; } // don't lose the error we got while trying to query by type - throw new QueryException($e->getMessage(), (int) $e->getCode(), $e); + throw new QueryException($e->getMessage(), (int)$e->getCode(), $e); } } throw $e; } - }, $constructor->getParameters())); + }, $constructor->getParameters()); } public function resolve($name) { @@ -105,8 +121,8 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer { if ($class->isInstantiable()) { return $this->buildClass($class); } else { - throw new QueryException($baseMsg . - ' Class can not be instantiated'); + throw new QueryException($baseMsg + . ' Class can not be instantiated'); } } catch (ReflectionException $e) { // Class does not exist @@ -153,13 +169,13 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer { return $closure($this); }; $name = $this->sanitizeName($name); - if (isset($this[$name])) { - unset($this[$name]); + if (isset($this->container[$name])) { + unset($this->container[$name]); } if ($shared) { - $this[$name] = $wrapped; + $this->container[$name] = $wrapped; } else { - $this[$name] = $this->container->factory($wrapped); + $this->container[$name] = $this->container->factory($wrapped); } } @@ -170,13 +186,28 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer { * @param string $alias the alias that should be registered * @param string $target the target that should be resolved instead */ - public function registerAlias($alias, $target) { - $this->registerService($alias, function (ContainerInterface $container) use ($target) { + public function registerAlias($alias, $target): void { + $this->registerService($alias, function (ContainerInterface $container) use ($target): mixed { + return $container->get($target); + }, false); + } + + protected function registerDeprecatedAlias(string $alias, string $target): void { + $this->registerService($alias, function (ContainerInterface $container) use ($target, $alias): mixed { + try { + $logger = $container->get(LoggerInterface::class); + $logger->debug('The requested alias "' . $alias . '" is deprecated. Please request "' . $target . '" directly. This alias will be removed in a future Nextcloud version.', [ + 'app' => $this->appName ?? 'serverDI', + ]); + } catch (ContainerExceptionInterface $e) { + // Could not get logger. Continue + } + return $container->get($target); }, false); } - /* + /** * @param string $name * @return string */ |