diff options
Diffstat (limited to 'lib/private/AppFramework')
24 files changed, 105 insertions, 333 deletions
diff --git a/lib/private/AppFramework/Bootstrap/Coordinator.php b/lib/private/AppFramework/Bootstrap/Coordinator.php index 2b04d291730..64e3dbfd928 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; @@ -69,26 +70,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 +178,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 c3b829825c2..95ad129c466 100644 --- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php +++ b/lib/private/AppFramework/Bootstrap/RegistrationContext.php @@ -157,7 +157,7 @@ class RegistrationContext { /** @var ServiceRegistration<\OCP\Files\Conversion\IConversionProvider>[] */ private array $fileConversionProviders = []; - + /** @var ServiceRegistration<IMailProvider>[] */ private $mailProviders = []; diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index b6e2df4ce7b..87361a9d1ea 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -440,7 +441,7 @@ class DIContainer extends SimpleContainer implements IAppContainer { 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/Request.php b/lib/private/AppFramework/Http/Request.php index d177221556c..e662cb8679a 100644 --- a/lib/private/AppFramework/Http/Request.php +++ b/lib/private/AppFramework/Http/Request.php @@ -45,7 +45,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { 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', @@ -64,6 +64,7 @@ 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: @@ -356,13 +357,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(); @@ -389,7 +390,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') { @@ -413,6 +421,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 diff --git a/lib/private/AppFramework/Middleware/FlowV2EphemeralSessionsMiddleware.php b/lib/private/AppFramework/Middleware/FlowV2EphemeralSessionsMiddleware.php index c30855a0e98..e4571dfc50e 100644 --- a/lib/private/AppFramework/Middleware/FlowV2EphemeralSessionsMiddleware.php +++ b/lib/private/AppFramework/Middleware/FlowV2EphemeralSessionsMiddleware.php @@ -33,8 +33,8 @@ class FlowV2EphemeralSessionsMiddleware extends Middleware { } if ( - $controller instanceof ClientFlowLoginV2Controller && - ($methodName === 'grantPage' || $methodName === 'generateAppPassword') + $controller instanceof ClientFlowLoginV2Controller + && ($methodName === 'grantPage' || $methodName === 'generateAppPassword') ) { return; } 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 2b3025fccff..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 @@ -120,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 40af67739d6..4453f5a7d4b 100644 --- a/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php @@ -68,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; @@ -134,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/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 d00840084a3..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 @@ -79,6 +80,9 @@ class PasswordConfirmationMiddleware extends Middleware { if ($this->isPasswordConfirmationStrict($reflectionMethod)) { $authHeader = $this->request->getHeader('Authorization'); + if (!str_starts_with(strtolower($authHeader), 'basic ')) { + throw new NotConfirmedException('Required authorization header missing'); + } [, $password] = explode(':', base64_decode(substr($authHeader, 6)), 2); $loginName = $this->session->get('loginname'); $loginResult = $this->userManager->checkPassword($loginName, $password); diff --git a/lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php b/lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php index efe56e0b124..ed3bb232023 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 diff --git a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php index 6b054b03f44..e3a293e0fd9 100644 --- a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php @@ -184,8 +184,8 @@ class SecurityMiddleware extends Middleware { } // 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(); } diff --git a/lib/private/AppFramework/OCS/BaseResponse.php b/lib/private/AppFramework/OCS/BaseResponse.php index 5929a3993ec..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 @@ -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 ''; diff --git a/lib/private/AppFramework/OCS/V1Response.php b/lib/private/AppFramework/OCS/V1Response.php index 131ca22ff24..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 diff --git a/lib/private/AppFramework/OCS/V2Response.php b/lib/private/AppFramework/OCS/V2Response.php index 47cf0f60200..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 diff --git a/lib/private/AppFramework/Routing/RouteConfig.php b/lib/private/AppFramework/Routing/RouteConfig.php deleted file mode 100644 index 2b7f21a8ba5..00000000000 --- a/lib/private/AppFramework/Routing/RouteConfig.php +++ /dev/null @@ -1,279 +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', - 'profile', - '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 894a74c727b..55e58234673 100644 --- a/lib/private/AppFramework/Routing/RouteParser.php +++ b/lib/private/AppFramework/Routing/RouteParser.php @@ -76,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'); } @@ -87,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 77c5ea4de0c..04d97738483 100644 --- a/lib/private/AppFramework/Services/AppConfig.php +++ b/lib/private/AppFramework/Services/AppConfig.php @@ -343,7 +343,7 @@ class AppConfig implements IAppConfig { * * @return array<string, string> */ - public function getAppInstalledVersions(): array { - return $this->appConfig->getAppInstalledVersions(); + public function getAppInstalledVersions(bool $onlyEnabled = false): array { + return $this->appConfig->getAppInstalledVersions($onlyEnabled); } } diff --git a/lib/private/AppFramework/Utility/SimpleContainer.php b/lib/private/AppFramework/Utility/SimpleContainer.php index 24918992ea3..1d77c277b02 100644 --- a/lib/private/AppFramework/Utility/SimpleContainer.php +++ b/lib/private/AppFramework/Utility/SimpleContainer.php @@ -12,6 +12,7 @@ use Closure; use OCP\AppFramework\QueryException; use OCP\IContainer; use Pimple\Container; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use ReflectionClass; use ReflectionException; @@ -23,8 +24,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 +51,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 +84,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,7 +97,7 @@ 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; @@ -95,7 +110,7 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer { throw $e; } - }, $constructor->getParameters())); + }, $constructor->getParameters()); } public function resolve($name) { @@ -105,8 +120,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 +168,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); } } |