diff options
author | John Molakvoæ <skjnldsv@users.noreply.github.com> | 2024-02-23 15:47:17 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-23 15:47:17 +0100 |
commit | b5357f7d12f89ce965cf8b8dd3bbc9cd0ad042c6 (patch) | |
tree | fb5982f6df0546adacb28cb67f96148dcfe78b33 /lib/private/AppFramework | |
parent | ce74bdcda244172cbe90dc792e30128802a78828 (diff) | |
parent | a88c1bdfb61d4c141d90e6864971f6d456417604 (diff) | |
download | nextcloud-server-b5357f7d12f89ce965cf8b8dd3bbc9cd0ad042c6.tar.gz nextcloud-server-b5357f7d12f89ce965cf8b8dd3bbc9cd0ad042c6.zip |
Merge branch 'master' into refactor/OC-Server-getThemingDefaults
Signed-off-by: John Molakvoæ <skjnldsv@users.noreply.github.com>
Diffstat (limited to 'lib/private/AppFramework')
28 files changed, 541 insertions, 156 deletions
diff --git a/lib/private/AppFramework/App.php b/lib/private/AppFramework/App.php index ffd77da888e..b18c95a2f0d 100644 --- a/lib/private/AppFramework/App.php +++ b/lib/private/AppFramework/App.php @@ -34,16 +34,16 @@ namespace OC\AppFramework; use OC\AppFramework\DependencyInjection\DIContainer; use OC\AppFramework\Http\Dispatcher; use OC\AppFramework\Http\Request; -use OCP\App\IAppManager; -use OCP\Profiler\IProfiler; use OC\Profiler\RoutingDataCollector; -use OCP\AppFramework\QueryException; +use OCP\App\IAppManager; use OCP\AppFramework\Http; use OCP\AppFramework\Http\ICallbackResponse; use OCP\AppFramework\Http\IOutput; +use OCP\AppFramework\QueryException; use OCP\Diagnostics\IEventLogger; use OCP\HintException; use OCP\IRequest; +use OCP\Profiler\IProfiler; /** * Entry point for every request in your app. You can consider this as your @@ -257,7 +257,7 @@ class App { * @param DIContainer $container an instance of a pimple container. */ public static function part(string $controllerName, string $methodName, array $urlParams, - DIContainer $container) { + DIContainer $container) { $container['urlParams'] = $urlParams; $controller = $container[$controllerName]; diff --git a/lib/private/AppFramework/Bootstrap/Coordinator.php b/lib/private/AppFramework/Bootstrap/Coordinator.php index f41b734a25b..8526a3dc1a1 100644 --- a/lib/private/AppFramework/Bootstrap/Coordinator.php +++ b/lib/private/AppFramework/Bootstrap/Coordinator.php @@ -30,20 +30,20 @@ declare(strict_types=1); namespace OC\AppFramework\Bootstrap; -use OCP\Diagnostics\IEventLogger; -use function class_exists; -use function class_implements; -use function in_array; -use OC_App; use OC\Support\CrashReport\Registry; +use OC_App; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\QueryException; use OCP\Dashboard\IManager; +use OCP\Diagnostics\IEventLogger; use OCP\EventDispatcher\IEventDispatcher; use OCP\IServerContainer; use Psr\Log\LoggerInterface; use Throwable; +use function class_exists; +use function class_implements; +use function in_array; class Coordinator { /** @var IServerContainer */ diff --git a/lib/private/AppFramework/Bootstrap/EventListenerRegistration.php b/lib/private/AppFramework/Bootstrap/EventListenerRegistration.php index 2ad410be26f..12801e62763 100644 --- a/lib/private/AppFramework/Bootstrap/EventListenerRegistration.php +++ b/lib/private/AppFramework/Bootstrap/EventListenerRegistration.php @@ -37,9 +37,9 @@ class EventListenerRegistration extends ServiceRegistration { private $priority; public function __construct(string $appId, - string $event, - string $service, - int $priority) { + string $event, + string $service, + int $priority) { parent::__construct($appId, $service); $this->event = $event; $this->priority = $priority; diff --git a/lib/private/AppFramework/Bootstrap/ParameterRegistration.php b/lib/private/AppFramework/Bootstrap/ParameterRegistration.php index b501a757abd..958f24cb600 100644 --- a/lib/private/AppFramework/Bootstrap/ParameterRegistration.php +++ b/lib/private/AppFramework/Bootstrap/ParameterRegistration.php @@ -36,8 +36,8 @@ final class ParameterRegistration extends ARegistration { private $value; public function __construct(string $appId, - string $name, - $value) { + string $name, + $value) { parent::__construct($appId); $this->name = $name; $this->value = $value; diff --git a/lib/private/AppFramework/Bootstrap/PreviewProviderRegistration.php b/lib/private/AppFramework/Bootstrap/PreviewProviderRegistration.php index 36c5cae7db3..e4d75f75bc8 100644 --- a/lib/private/AppFramework/Bootstrap/PreviewProviderRegistration.php +++ b/lib/private/AppFramework/Bootstrap/PreviewProviderRegistration.php @@ -34,8 +34,8 @@ class PreviewProviderRegistration extends ServiceRegistration { private $mimeTypeRegex; public function __construct(string $appId, - string $service, - string $mimeTypeRegex) { + string $service, + string $mimeTypeRegex) { parent::__construct($appId, $service); $this->mimeTypeRegex = $mimeTypeRegex; } diff --git a/lib/private/AppFramework/Bootstrap/RegistrationContext.php b/lib/private/AppFramework/Bootstrap/RegistrationContext.php index 5aea2a7a744..120ee7ea9fa 100644 --- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php +++ b/lib/private/AppFramework/Bootstrap/RegistrationContext.php @@ -30,15 +30,6 @@ declare(strict_types=1); namespace OC\AppFramework\Bootstrap; use Closure; -use OCP\Calendar\Resource\IBackend as IResourceBackend; -use OCP\Calendar\Room\IBackend as IRoomBackend; -use OCP\Collaboration\Reference\IReferenceProvider; -use OCP\TextProcessing\IProvider as ITextProcessingProvider; -use OCP\SpeechToText\ISpeechToTextProvider; -use OCP\Talk\ITalkBackend; -use OCP\Translation\ITranslationProvider; -use RuntimeException; -use function array_shift; use OC\Support\CrashReport\Registry; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IRegistrationContext; @@ -46,7 +37,10 @@ use OCP\AppFramework\Middleware; use OCP\AppFramework\Services\InitialStateProvider; use OCP\Authentication\IAlternativeLogin; use OCP\Calendar\ICalendarProvider; +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\Dashboard\IManager; use OCP\Dashboard\IWidget; use OCP\EventDispatcher\IEventDispatcher; @@ -55,11 +49,18 @@ use OCP\Http\WellKnown\IHandler; use OCP\Notification\INotifier; use OCP\Profile\ILinkAction; use OCP\Search\IProvider; +use OCP\SetupCheck\ISetupCheck; use OCP\Share\IPublicShareTemplateProvider; +use OCP\SpeechToText\ISpeechToTextProvider; use OCP\Support\CrashReport\IReporter; +use OCP\Talk\ITalkBackend; +use OCP\TextProcessing\IProvider as ITextProcessingProvider; +use OCP\Translation\ITranslationProvider; use OCP\UserMigration\IMigrator as IUserMigrator; use Psr\Log\LoggerInterface; +use RuntimeException; use Throwable; +use function array_shift; class RegistrationContext { /** @var ServiceRegistration<ICapability>[] */ @@ -137,6 +138,9 @@ class RegistrationContext { /** @var ServiceRegistration<IReferenceProvider>[] */ private array $referenceProviders = []; + /** @var ServiceRegistration<\OCP\TextToImage\IProvider>[] */ + private $textToImageProviders = []; + @@ -146,11 +150,13 @@ class RegistrationContext { /** @var ServiceRegistration<IPublicShareTemplateProvider>[] */ private $publicShareTemplateProviders = []; - /** @var LoggerInterface */ - private $logger; + private LoggerInterface $logger; + + /** @var ServiceRegistration<ISetupCheck>[] */ + private array $setupChecks = []; /** @var PreviewProviderRegistration[] */ - private $previewProviders = []; + private array $previewProviders = []; public function __construct(LoggerInterface $logger) { $this->logger = $logger; @@ -273,6 +279,13 @@ class RegistrationContext { ); } + public function registerTextToImageProvider(string $providerClass): void { + $this->context->registerTextToImageProvider( + $this->appId, + $providerClass + ); + } + public function registerTemplateProvider(string $providerClass): void { $this->context->registerTemplateProvider( $this->appId, @@ -372,6 +385,13 @@ class RegistrationContext { $class ); } + + public function registerSetupCheck(string $setupCheckClass): void { + $this->context->registerSetupCheck( + $this->appId, + $setupCheckClass + ); + } }; } @@ -383,14 +403,14 @@ class RegistrationContext { } /** - * @psalm-param class-string<IReporter> $capability + * @psalm-param class-string<IReporter> $reporterClass */ public function registerCrashReporter(string $appId, string $reporterClass): void { $this->crashReporters[] = new ServiceRegistration($appId, $reporterClass); } /** - * @psalm-param class-string<IWidget> $capability + * @psalm-param class-string<IWidget> $panelClass */ public function registerDashboardPanel(string $appId, string $panelClass): void { $this->dashboardPanels[] = new ServiceRegistration($appId, $panelClass); @@ -443,6 +463,10 @@ class RegistrationContext { $this->textProcessingProviders[] = new ServiceRegistration($appId, $class); } + public function registerTextToImageProvider(string $appId, string $class): void { + $this->textToImageProviders[] = new ServiceRegistration($appId, $class); + } + public function registerTemplateProvider(string $appId, string $class): void { $this->templateProviders[] = new ServiceRegistration($appId, $class); } @@ -524,6 +548,13 @@ class RegistrationContext { } /** + * @psalm-param class-string<ISetupCheck> $setupCheckClass + */ + public function registerSetupCheck(string $appId, string $setupCheckClass): void { + $this->setupChecks[] = new ServiceRegistration($appId, $setupCheckClass); + } + + /** * @param App[] $apps */ public function delegateCapabilityRegistrations(array $apps): void { @@ -565,9 +596,6 @@ class RegistrationContext { } } - /** - * @param App[] $apps - */ public function delegateDashboardPanelRegistrations(IManager $dashboardManager): void { while (($panel = array_shift($this->dashboardPanels)) !== null) { try { @@ -729,6 +757,13 @@ class RegistrationContext { } /** + * @return ServiceRegistration<\OCP\TextToImage\IProvider>[] + */ + public function getTextToImageProviders(): array { + return $this->textToImageProviders; + } + + /** * @return ServiceRegistration<ICustomTemplateProvider>[] */ public function getTemplateProviders(): array { @@ -828,4 +863,11 @@ class RegistrationContext { public function getPublicShareTemplateProviders(): array { return $this->publicShareTemplateProviders; } + + /** + * @return ServiceRegistration<ISetupCheck>[] + */ + public function getSetupChecks(): array { + return $this->setupChecks; + } } diff --git a/lib/private/AppFramework/Bootstrap/ServiceAliasRegistration.php b/lib/private/AppFramework/Bootstrap/ServiceAliasRegistration.php index e2b115e0353..62c7169a7ee 100644 --- a/lib/private/AppFramework/Bootstrap/ServiceAliasRegistration.php +++ b/lib/private/AppFramework/Bootstrap/ServiceAliasRegistration.php @@ -46,8 +46,8 @@ class ServiceAliasRegistration extends ARegistration { * @paslm-param string|class-string $target */ public function __construct(string $appId, - string $alias, - string $target) { + string $alias, + string $target) { parent::__construct($appId); $this->alias = $alias; $this->target = $target; diff --git a/lib/private/AppFramework/Bootstrap/ServiceFactoryRegistration.php b/lib/private/AppFramework/Bootstrap/ServiceFactoryRegistration.php index b6658e55239..9d166526d94 100644 --- a/lib/private/AppFramework/Bootstrap/ServiceFactoryRegistration.php +++ b/lib/private/AppFramework/Bootstrap/ServiceFactoryRegistration.php @@ -45,9 +45,9 @@ class ServiceFactoryRegistration extends ARegistration { private $shared; public function __construct(string $appId, - string $alias, - callable $target, - bool $shared) { + string $alias, + callable $target, + bool $shared) { parent::__construct($appId); $this->name = $alias; $this->factory = $target; diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index e4a06d9d4e0..a5273d2f335 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -345,6 +345,7 @@ class DIContainer extends SimpleContainer implements IAppContainer { $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') ); }); @@ -404,33 +405,6 @@ class DIContainer extends SimpleContainer implements IAppContainer { } /** - * @deprecated use the ILogger instead - * @param string $message - * @param string $level - * @return mixed - */ - public function log($message, $level) { - switch ($level) { - case 'debug': - $level = ILogger::DEBUG; - break; - case 'info': - $level = ILogger::INFO; - break; - case 'warn': - $level = ILogger::WARN; - break; - case 'fatal': - $level = ILogger::FATAL; - break; - default: - $level = ILogger::ERROR; - break; - } - \OCP\Util::writeLog($this->getAppName(), $message, $level); - } - - /** * Register a capability * * @param string $serviceName e.g. 'OCA\Files\Capabilities' diff --git a/lib/private/AppFramework/Http/Dispatcher.php b/lib/private/AppFramework/Http/Dispatcher.php index 13b391eb287..6e946f2bfa3 100644 --- a/lib/private/AppFramework/Http/Dispatcher.php +++ b/lib/private/AppFramework/Http/Dispatcher.php @@ -38,6 +38,7 @@ use OC\AppFramework\Utility\ControllerMethodReflector; use OC\DB\ConnectionAdapter; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\ParameterOutOfRangeException; use OCP\AppFramework\Http\Response; use OCP\Diagnostics\IEventLogger; use OCP\IConfig; @@ -88,14 +89,14 @@ class Dispatcher { * @param IEventLogger $eventLogger */ public function __construct(Http $protocol, - MiddlewareDispatcher $middlewareDispatcher, - ControllerMethodReflector $reflector, - IRequest $request, - IConfig $config, - ConnectionAdapter $connection, - LoggerInterface $logger, - IEventLogger $eventLogger, - ContainerInterface $appContainer) { + MiddlewareDispatcher $middlewareDispatcher, + ControllerMethodReflector $reflector, + IRequest $request, + IConfig $config, + ConnectionAdapter $connection, + LoggerInterface $logger, + IEventLogger $eventLogger, + ContainerInterface $appContainer) { $this->protocol = $protocol; $this->middlewareDispatcher = $middlewareDispatcher; $this->reflector = $reflector; @@ -197,7 +198,7 @@ class Dispatcher { private function executeController(Controller $controller, string $methodName): Response { $arguments = []; - // valid types that will be casted + // valid types that will be cast $types = ['int', 'integer', 'bool', 'boolean', 'float', 'double']; foreach ($this->reflector->getParameters() as $param => $default) { @@ -219,6 +220,7 @@ class Dispatcher { $value = false; } elseif ($value !== null && \in_array($type, $types, true)) { settype($value, $type); + $this->ensureParameterValueSatisfiesRange($param, $value); } elseif ($value === null && $type !== null && $this->appContainer->has($type)) { $value = $this->appContainer->get($type); } @@ -250,4 +252,22 @@ class Dispatcher { return $response; } + + /** + * @psalm-param mixed $value + * @throws ParameterOutOfRangeException + */ + private function ensureParameterValueSatisfiesRange(string $param, $value): void { + $rangeInfo = $this->reflector->getRange($param); + if ($rangeInfo) { + if ($value < $rangeInfo['min'] || $value > $rangeInfo['max']) { + throw new ParameterOutOfRangeException( + $param, + $value, + $rangeInfo['min'], + $rangeInfo['max'], + ); + } + } + } } diff --git a/lib/private/AppFramework/Http/Request.php b/lib/private/AppFramework/Http/Request.php index 408e88583a0..b905c6184fa 100644 --- a/lib/private/AppFramework/Http/Request.php +++ b/lib/private/AppFramework/Http/Request.php @@ -63,6 +63,7 @@ use Symfony\Component\HttpFoundation\IpUtils; * @property string method * @property mixed[] parameters * @property mixed[] server + * @template-implements \ArrayAccess<string,mixed> */ class Request implements \ArrayAccess, \Countable, IRequest { public const USER_AGENT_IE = '/(MSIE)|(Trident)/'; @@ -118,10 +119,10 @@ class Request implements \ArrayAccess, \Countable, IRequest { * @see https://www.php.net/manual/en/reserved.variables.php */ public function __construct(array $vars, - IRequestId $requestId, - IConfig $config, - CsrfTokenManager $csrfTokenManager = null, - string $stream = 'php://input') { + IRequestId $requestId, + IConfig $config, + CsrfTokenManager $csrfTokenManager = null, + string $stream = 'php://input') { $this->inputStream = $stream; $this->items['params'] = []; $this->requestId = $requestId; @@ -193,9 +194,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { */ #[\ReturnTypeWillChange] public function offsetGet($offset) { - return isset($this->items['parameters'][$offset]) - ? $this->items['parameters'][$offset] - : null; + return $this->items['parameters'][$offset] ?? null; } /** @@ -255,9 +254,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { case 'cookies': case 'urlParams': case 'method': - return isset($this->items[$name]) - ? $this->items[$name] - : null; + return $this->items[$name] ?? null; case 'parameters': case 'params': if ($this->isPutStreamContent()) { @@ -577,7 +574,14 @@ class Request implements \ArrayAccess, \Countable, IRequest { * @return boolean true if $remoteAddress matches any entry in $trustedProxies, false otherwise */ protected function isTrustedProxy($trustedProxies, $remoteAddress) { - return IpUtils::checkIp($remoteAddress, $trustedProxies); + try { + return IpUtils::checkIp($remoteAddress, $trustedProxies); + } catch (\Throwable) { + // We can not log to our log here as the logger is using `getRemoteAddress` which uses the function, so we would have a cyclic dependency + // Reaching this line means `trustedProxies` is in invalid format. + error_log('Nextcloud trustedProxies has malformed entries'); + return false; + } } /** @@ -597,14 +601,25 @@ class Request implements \ArrayAccess, \Countable, IRequest { // only have one default, so we cannot ship an insecure product out of the box ]); - foreach ($forwardedForHeaders as $header) { + // Read the x-forwarded-for headers and values in reverse order as per + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#selecting_an_ip_address + foreach (array_reverse($forwardedForHeaders) as $header) { if (isset($this->server[$header])) { - foreach (explode(',', $this->server[$header]) as $IP) { + foreach (array_reverse(explode(',', $this->server[$header])) as $IP) { $IP = trim($IP); + $colons = substr_count($IP, ':'); + if ($colons > 1) { + // Extract IP from string with brackets and optional port + if (preg_match('/^\[(.+?)\](?::\d+)?$/', $IP, $matches) && isset($matches[1])) { + $IP = $matches[1]; + } + } elseif ($colons === 1) { + // IPv4 with port + $IP = substr($IP, 0, strpos($IP, ':')); + } - // remove brackets from IPv6 addresses - if (str_starts_with($IP, '[') && str_ends_with($IP, ']')) { - $IP = substr($IP, 1, -1); + if ($this->isTrustedProxy($trustedProxies, $IP)) { + continue; } if (filter_var($IP, FILTER_VALIDATE_IP) !== false) { @@ -620,14 +635,12 @@ class Request implements \ArrayAccess, \Countable, IRequest { /** * Check overwrite condition - * @param string $type * @return bool */ - private function isOverwriteCondition(string $type = ''): bool { + private function isOverwriteCondition(): bool { $regex = '/' . $this->config->getSystemValueString('overwritecondaddr', '') . '/'; $remoteAddr = isset($this->server['REMOTE_ADDR']) ? $this->server['REMOTE_ADDR'] : ''; - return $regex === '//' || preg_match($regex, $remoteAddr) === 1 - || $type !== 'protocol'; + return $regex === '//' || preg_match($regex, $remoteAddr) === 1; } /** @@ -637,7 +650,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { */ public function getServerProtocol(): string { if ($this->config->getSystemValueString('overwriteprotocol') !== '' - && $this->isOverwriteCondition('protocol')) { + && $this->isOverwriteCondition()) { return $this->config->getSystemValueString('overwriteprotocol'); } diff --git a/lib/private/AppFramework/Http/RequestId.php b/lib/private/AppFramework/Http/RequestId.php index 70032873a75..a6b24c0a2ff 100644 --- a/lib/private/AppFramework/Http/RequestId.php +++ b/lib/private/AppFramework/Http/RequestId.php @@ -31,7 +31,7 @@ class RequestId implements IRequestId { protected string $requestId; public function __construct(string $uniqueId, - ISecureRandom $secureRandom) { + ISecureRandom $secureRandom) { $this->requestId = $uniqueId; $this->secureRandom = $secureRandom; } diff --git a/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php b/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php index 35eb0098eed..e129f70aef6 100644 --- a/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php +++ b/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php @@ -40,15 +40,15 @@ use OCP\AppFramework\Middleware; */ class MiddlewareDispatcher { /** - * @var array array containing all the middlewares + * @var Middleware[] array containing all the middlewares */ - private $middlewares; + private array $middlewares; /** * @var int counter which tells us what middleware was executed once an * exception occurs */ - private $middlewareCounter; + private int $middlewareCounter; /** @@ -64,14 +64,14 @@ class MiddlewareDispatcher { * Adds a new middleware * @param Middleware $middleWare the middleware which will be added */ - public function registerMiddleware(Middleware $middleWare) { + public function registerMiddleware(Middleware $middleWare): void { $this->middlewares[] = $middleWare; } /** * returns an array with all middleware elements - * @return array the middlewares + * @return Middleware[] the middlewares */ public function getMiddlewares(): array { return $this->middlewares; @@ -86,7 +86,7 @@ class MiddlewareDispatcher { * @param string $methodName the name of the method that will be called on * the controller */ - public function beforeController(Controller $controller, string $methodName) { + public function beforeController(Controller $controller, string $methodName): void { // we need to count so that we know which middlewares we have to ask in // case there is an exception $middlewareCount = \count($this->middlewares); diff --git a/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php index 8bdacf550b6..fef9632487e 100644 --- a/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php @@ -38,6 +38,7 @@ use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\Response; use OCP\AppFramework\Middleware; use OCP\IRequest; +use OCP\ISession; use OCP\Security\Bruteforce\IThrottler; use ReflectionMethod; @@ -58,9 +59,9 @@ class CORSMiddleware extends Middleware { private $throttler; public function __construct(IRequest $request, - ControllerMethodReflector $reflector, - Session $session, - IThrottler $throttler) { + ControllerMethodReflector $reflector, + Session $session, + IThrottler $throttler) { $this->request = $request; $this->reflector = $reflector; $this->session = $session; @@ -91,6 +92,10 @@ class CORSMiddleware extends Middleware { if ($this->request->passesCSRFCheck()) { return; } + // Skip CORS check for requests with AppAPI auth. + if ($this->session->getSession() instanceof ISession && $this->session->getSession()->get('app_api') === true) { + return; + } $this->session->logout(); try { if ($user === null || $pass === null || !$this->session->logClientIn($user, $pass, $this->request, $this->throttler)) { diff --git a/lib/private/AppFramework/Middleware/Security/CSPMiddleware.php b/lib/private/AppFramework/Middleware/Security/CSPMiddleware.php index ae0dc1f134e..60a7cef8fa1 100644 --- a/lib/private/AppFramework/Middleware/Security/CSPMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/CSPMiddleware.php @@ -44,8 +44,8 @@ class CSPMiddleware extends Middleware { private $csrfTokenManager; public function __construct(ContentSecurityPolicyManager $policyManager, - ContentSecurityPolicyNonceManager $cspNonceManager, - CsrfTokenManager $csrfTokenManager) { + ContentSecurityPolicyNonceManager $cspNonceManager, + CsrfTokenManager $csrfTokenManager) { $this->contentSecurityPolicyManager = $policyManager; $this->cspNonceManager = $cspNonceManager; $this->csrfTokenManager = $csrfTokenManager; diff --git a/lib/private/AppFramework/Middleware/Security/Exceptions/SecurityException.php b/lib/private/AppFramework/Middleware/Security/Exceptions/SecurityException.php index 3232980b7e5..3b2296c145f 100644 --- a/lib/private/AppFramework/Middleware/Security/Exceptions/SecurityException.php +++ b/lib/private/AppFramework/Middleware/Security/Exceptions/SecurityException.php @@ -1,4 +1,7 @@ <?php + +declare(strict_types=1); + /** * @copyright Copyright (c) 2016, ownCloud, Inc. * diff --git a/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php b/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php index a72a7a40016..351f47ea924 100644 --- a/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php @@ -55,9 +55,9 @@ class PasswordConfirmationMiddleware extends Middleware { * @param ITimeFactory $timeFactory */ public function __construct(ControllerMethodReflector $reflector, - ISession $session, - IUserSession $userSession, - ITimeFactory $timeFactory) { + ISession $session, + IUserSession $userSession, + ITimeFactory $timeFactory) { $this->reflector = $reflector; $this->session = $session; $this->userSession = $userSession; diff --git a/lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php b/lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php index e6d35dc66f2..870efdd44fa 100644 --- a/lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php @@ -38,7 +38,7 @@ class SameSiteCookieMiddleware extends Middleware { private $reflector; public function __construct(Request $request, - ControllerMethodReflector $reflector) { + ControllerMethodReflector $reflector) { $this->request = $request; $this->reflector = $reflector; } diff --git a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php index db6c7a02c77..386075bd968 100644 --- a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php @@ -104,18 +104,18 @@ class SecurityMiddleware extends Middleware { 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 + ControllerMethodReflector $reflector, + INavigationManager $navigationManager, + IURLGenerator $urlGenerator, + LoggerInterface $logger, + string $appName, + bool $isLoggedIn, + bool $isAdminUser, + bool $isSubAdmin, + IAppManager $appManager, + IL10N $l10n, + AuthorizedGroupMapper $mapper, + IUserSession $userSession ) { $this->navigationManager = $navigationManager; $this->request = $request; @@ -180,20 +180,20 @@ class SecurityMiddleware extends Middleware { } } if (!$authorized) { - throw new NotAdminException($this->l10n->t('Logged in user must be an admin, a sub admin or gotten special right to access this setting')); + 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->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class) && !$this->isSubAdmin && !$this->isAdminUser && !$authorized) { - throw new NotAdminException($this->l10n->t('Logged in user must be an admin or sub admin')); + 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 && !$authorized) { - throw new NotAdminException($this->l10n->t('Logged in user must be an admin')); + throw new NotAdminException($this->l10n->t('Logged in account must be an admin')); } } diff --git a/lib/private/AppFramework/Middleware/SessionMiddleware.php b/lib/private/AppFramework/Middleware/SessionMiddleware.php index 39f85915901..0acdcf8b7ef 100644 --- a/lib/private/AppFramework/Middleware/SessionMiddleware.php +++ b/lib/private/AppFramework/Middleware/SessionMiddleware.php @@ -44,7 +44,7 @@ class SessionMiddleware extends Middleware { private $session; public function __construct(ControllerMethodReflector $reflector, - ISession $session) { + ISession $session) { $this->reflector = $reflector; $this->session = $session; } diff --git a/lib/private/AppFramework/OCS/BaseResponse.php b/lib/private/AppFramework/OCS/BaseResponse.php index 123b73d302c..3cfe8177ae7 100644 --- a/lib/private/AppFramework/OCS/BaseResponse.php +++ b/lib/private/AppFramework/OCS/BaseResponse.php @@ -64,10 +64,10 @@ abstract class BaseResponse extends Response { * @param int|null $itemsPerPage */ public function __construct(DataResponse $dataResponse, - $format = 'xml', - $statusMessage = null, - $itemsCount = null, - $itemsPerPage = null) { + $format = 'xml', + $statusMessage = null, + $itemsCount = null, + $itemsPerPage = null) { parent::__construct(); $this->format = $format; diff --git a/lib/private/AppFramework/Routing/RouteConfig.php b/lib/private/AppFramework/Routing/RouteConfig.php index 6e3e49e8d99..7d63e5477ce 100644 --- a/lib/private/AppFramework/Routing/RouteConfig.php +++ b/lib/private/AppFramework/Routing/RouteConfig.php @@ -136,7 +136,13 @@ class RouteConfig { $controllerName = $this->buildControllerName($controller); $actionName = $this->buildActionName($action); - $routeName = $routeNamePrefix . $this->appName . '.' . $controller . '.' . $action . $postfix; + /* + * 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); diff --git a/lib/private/AppFramework/Routing/RouteParser.php b/lib/private/AppFramework/Routing/RouteParser.php index 1b3a6c1255a..1b05c23df9d 100644 --- a/lib/private/AppFramework/Routing/RouteParser.php +++ b/lib/private/AppFramework/Routing/RouteParser.php @@ -100,7 +100,13 @@ class RouteParser { $controllerName = $this->buildControllerName($controller); $actionName = $this->buildActionName($action); - $routeName = $routeNamePrefix . $appName . '.' . $controller . '.' . $action . $postfix; + /* + * 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 . $appName . '.' . $controller . '.' . $action . $postfix); $routeObject = new Route($url); $routeObject->method($verb); diff --git a/lib/private/AppFramework/ScopedPsrLogger.php b/lib/private/AppFramework/ScopedPsrLogger.php index 4ed91cdb6c0..1cb58da11ef 100644 --- a/lib/private/AppFramework/ScopedPsrLogger.php +++ b/lib/private/AppFramework/ScopedPsrLogger.php @@ -37,7 +37,7 @@ class ScopedPsrLogger implements LoggerInterface { private $appId; public function __construct(LoggerInterface $inner, - string $appId) { + string $appId) { $this->inner = $inner; $this->appId = $appId; } diff --git a/lib/private/AppFramework/Services/AppConfig.php b/lib/private/AppFramework/Services/AppConfig.php index 1fc07bc22b0..1d18baef9ed 100644 --- a/lib/private/AppFramework/Services/AppConfig.php +++ b/lib/private/AppFramework/Services/AppConfig.php @@ -26,39 +26,322 @@ declare(strict_types=1); */ namespace OC\AppFramework\Services; +use InvalidArgumentException; +use JsonException; use OCP\AppFramework\Services\IAppConfig; +use OCP\Exceptions\AppConfigTypeConflictException; +use OCP\Exceptions\AppConfigUnknownKeyException; use OCP\IConfig; class AppConfig implements IAppConfig { - /** @var IConfig */ - private $config; + public function __construct( + private IConfig $config, + /** @var \OC\AppConfig */ + private \OCP\IAppConfig $appConfig, + private string $appName, + ) { + } - /** @var string */ - private $appName; + /** + * @inheritDoc + * + * @return string[] list of stored config keys + * @since 20.0.0 + */ + public function getAppKeys(): array { + return $this->appConfig->getKeys($this->appName); + } - public function __construct(IConfig $config, string $appName) { - $this->config = $config; - $this->appName = $appName; + /** + * @inheritDoc + * + * @param string $key config key + * @param bool|null $lazy TRUE to search within lazy loaded config, NULL to search within all config + * + * @return bool TRUE if key exists + * @since 29.0.0 + */ + public function hasAppKey(string $key, ?bool $lazy = false): bool { + return $this->appConfig->hasKey($this->appName, $key, $lazy); } - public function getAppKeys(): array { - return $this->config->getAppKeys($this->appName); + /** + * @param string $key config key + * @param bool|null $lazy TRUE to search within lazy loaded config, NULL to search within all config + * + * @return bool + * @throws AppConfigUnknownKeyException if config key is not known + * @since 29.0.0 + */ + public function isSensitive(string $key, ?bool $lazy = false): bool { + return $this->appConfig->isSensitive($this->appName, $key, $lazy); + } + + /** + * @inheritDoc + * + * @param string $key config key + * + * @return bool TRUE if config is lazy loaded + * @throws AppConfigUnknownKeyException if config key is not known + * @see \OCP\IAppConfig for details about lazy loading + * @since 29.0.0 + */ + public function isLazy(string $key): bool { + return $this->appConfig->isLazy($this->appName, $key); + } + + /** + * @inheritDoc + * + * @param string $key config keys prefix to search + * @param bool $filtered TRUE to hide sensitive config values. Value are replaced by {@see IConfig::SENSITIVE_VALUE} + * + * @return array<string, string> [configKey => configValue] + * @since 29.0.0 + */ + public function getAllAppValues(string $key = '', bool $filtered = false): array { + return $this->appConfig->getAllValues($this->appName, $key, $filtered); } + /** + * @inheritDoc + * + * @param string $key the key of the value, under which will be saved + * @param string $value the value that should be stored + * @since 20.0.0 + * @deprecated 29.0.0 use {@see setAppValueString()} + */ public function setAppValue(string $key, string $value): void { - $this->config->setAppValue($this->appName, $key, $value); + /** @psalm-suppress InternalMethod */ + $this->appConfig->setValueMixed($this->appName, $key, $value); + } + + /** + * @inheritDoc + * + * @param string $key config key + * @param string $value config value + * @param bool $lazy set config as lazy loaded + * @param bool $sensitive if TRUE value will be hidden when listing config values. + * + * @return bool TRUE if value was different, therefor updated in database + * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one + * @since 29.0.0 + * @see \OCP\IAppConfig for explanation about lazy loading + */ + public function setAppValueString( + string $key, + string $value, + bool $lazy = false, + bool $sensitive = false + ): bool { + return $this->appConfig->setValueString($this->appName, $key, $value, $lazy, $sensitive); + } + + /** + * @inheritDoc + * + * @param string $key config key + * @param int $value config value + * @param bool $lazy set config as lazy loaded + * @param bool $sensitive if TRUE value will be hidden when listing config values. + * + * @return bool TRUE if value was different, therefor updated in database + * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one + * @since 29.0.0 + * @see \OCP\IAppConfig for explanation about lazy loading + */ + public function setAppValueInt( + string $key, + int $value, + bool $lazy = false, + bool $sensitive = false + ): bool { + return $this->appConfig->setValueInt($this->appName, $key, $value, $lazy, $sensitive); + } + + /** + * @inheritDoc + * + * @param string $key config key + * @param float $value config value + * @param bool $lazy set config as lazy loaded + * @param bool $sensitive if TRUE value will be hidden when listing config values. + * + * @return bool TRUE if value was different, therefor updated in database + * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one + * @since 29.0.0 + * @see \OCP\IAppConfig for explanation about lazy loading + */ + public function setAppValueFloat( + string $key, + float $value, + bool $lazy = false, + bool $sensitive = false + ): bool { + return $this->appConfig->setValueFloat($this->appName, $key, $value, $lazy, $sensitive); } + /** + * @inheritDoc + * + * @param string $key config key + * @param bool $value config value + * @param bool $lazy set config as lazy loaded + * + * @return bool TRUE if value was different, therefor updated in database + * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one + * @since 29.0.0 + * @see \OCP\IAppConfig for explanation about lazy loading + */ + public function setAppValueBool( + string $key, + bool $value, + bool $lazy = false + ): bool { + return $this->appConfig->setValueBool($this->appName, $key, $value, $lazy); + } + + /** + * @inheritDoc + * + * @param string $key config key + * @param array $value config value + * @param bool $lazy set config as lazy loaded + * @param bool $sensitive if TRUE value will be hidden when listing config values. + * + * @return bool TRUE if value was different, therefor updated in database + * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one + * @throws JsonException + * @since 29.0.0 + * @see \OCP\IAppConfig for explanation about lazy loading + */ + public function setAppValueArray( + string $key, + array $value, + bool $lazy = false, + bool $sensitive = false + ): bool { + return $this->appConfig->setValueArray($this->appName, $key, $value, $lazy, $sensitive); + } + + /** + * @param string $key + * @param string $default + * + * @since 20.0.0 + * @deprecated 29.0.0 use {@see getAppValueString()} + * @return string + */ public function getAppValue(string $key, string $default = ''): string { - return $this->config->getAppValue($this->appName, $key, $default); + /** @psalm-suppress InternalMethod */ + /** @psalm-suppress UndefinedInterfaceMethod */ + return $this->appConfig->getValueMixed($this->appName, $key, $default); + } + + /** + * @inheritDoc + * + * @param string $key config key + * @param string $default default value + * @param bool $lazy search within lazy loaded config + * + * @return string stored config value or $default if not set in database + * @throws InvalidArgumentException if one of the argument format is invalid + * @throws AppConfigTypeConflictException in case of conflict with the value type set in database + * @since 29.0.0 + * @see \OCP\IAppConfig for explanation about lazy loading + */ + public function getAppValueString(string $key, string $default = '', bool $lazy = false): string { + return $this->appConfig->getValueString($this->appName, $key, $default, $lazy); + } + + /** + * @inheritDoc + * + * @param string $key config key + * @param int $default default value + * @param bool $lazy search within lazy loaded config + * + * @return int stored config value or $default if not set in database + * @throws InvalidArgumentException if one of the argument format is invalid + * @throws AppConfigTypeConflictException in case of conflict with the value type set in database + * @since 29.0.0 + * @see \OCP\IAppConfig for explanation about lazy loading + */ + public function getAppValueInt(string $key, int $default = 0, bool $lazy = false): int { + return $this->appConfig->getValueInt($this->appName, $key, $default, $lazy); + } + + /** + * @inheritDoc + * + * @param string $key config key + * @param float $default default value + * @param bool $lazy search within lazy loaded config + * + * @return float stored config value or $default if not set in database + * @throws InvalidArgumentException if one of the argument format is invalid + * @throws AppConfigTypeConflictException in case of conflict with the value type set in database + * @since 29.0.0 + * @see \OCP\IAppConfig for explanation about lazy loading + */ + public function getAppValueFloat(string $key, float $default = 0, bool $lazy = false): float { + return $this->appConfig->getValueFloat($this->appName, $key, $default, $lazy); + } + + /** + * @inheritDoc + * + * @param string $key config key + * @param bool $default default value + * @param bool $lazy search within lazy loaded config + * + * @return bool stored config value or $default if not set in database + * @throws InvalidArgumentException if one of the argument format is invalid + * @throws AppConfigTypeConflictException in case of conflict with the value type set in database + * @since 29.0.0 + * @see \OCP\IAppConfig for explanation about lazy loading + */ + public function getAppValueBool(string $key, bool $default = false, bool $lazy = false): bool { + return $this->appConfig->getValueBool($this->appName, $key, $default, $lazy); + } + + /** + * @inheritDoc + * + * @param string $key config key + * @param array $default default value + * @param bool $lazy search within lazy loaded config + * + * @return array stored config value or $default if not set in database + * @throws InvalidArgumentException if one of the argument format is invalid + * @throws AppConfigTypeConflictException in case of conflict with the value type set in database + * @since 29.0.0 + * @see \OCP\IAppConfig for explanation about lazy loading + */ + public function getAppValueArray(string $key, array $default = [], bool $lazy = false): array { + return $this->appConfig->getValueArray($this->appName, $key, $default, $lazy); } + /** + * @inheritDoc + * + * @param string $key the key of the value, under which it was saved + * @since 20.0.0 + */ public function deleteAppValue(string $key): void { - $this->config->deleteAppValue($this->appName, $key); + $this->appConfig->deleteKey($this->appName, $key); } + /** + * @inheritDoc + * + * @since 20.0.0 + */ public function deleteAppValues(): void { - $this->config->deleteAppValues($this->appName); + $this->appConfig->deleteApp($this->appName); } public function setUserValue(string $userId, string $key, string $value, ?string $preCondition = null): void { diff --git a/lib/private/AppFramework/Utility/ControllerMethodReflector.php b/lib/private/AppFramework/Utility/ControllerMethodReflector.php index b76b3c33c42..bd68dd96ed4 100644 --- a/lib/private/AppFramework/Utility/ControllerMethodReflector.php +++ b/lib/private/AppFramework/Utility/ControllerMethodReflector.php @@ -42,6 +42,7 @@ class ControllerMethodReflector implements IControllerMethodReflector { public $annotations = []; private $types = []; private $parameters = []; + private array $ranges = []; /** * @param object $object an object or classname @@ -54,26 +55,38 @@ class ControllerMethodReflector implements IControllerMethodReflector { if ($docs !== false) { // extract everything prefixed by @ and first letter uppercase preg_match_all('/^\h+\*\h+@(?P<annotation>[A-Z]\w+)((?P<parameter>.*))?$/m', $docs, $matches); - foreach ($matches['annotation'] as $key => $annontation) { - $annontation = strtolower($annontation); + foreach ($matches['annotation'] as $key => $annotation) { + $annotation = strtolower($annotation); $annotationValue = $matches['parameter'][$key]; - if (isset($annotationValue[0]) && $annotationValue[0] === '(' && $annotationValue[\strlen($annotationValue) - 1] === ')') { + if (str_starts_with($annotationValue, '(') && str_ends_with($annotationValue, ')')) { $cutString = substr($annotationValue, 1, -1); $cutString = str_replace(' ', '', $cutString); - $splittedArray = explode(',', $cutString); - foreach ($splittedArray as $annotationValues) { + $splitArray = explode(',', $cutString); + foreach ($splitArray as $annotationValues) { [$key, $value] = explode('=', $annotationValues); - $this->annotations[$annontation][$key] = $value; + $this->annotations[$annotation][$key] = $value; } continue; } - $this->annotations[$annontation] = [$annotationValue]; + $this->annotations[$annotation] = [$annotationValue]; } // 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); + foreach ($matches['var'] as $index => $varName) { + if ($matches['type'][$index] !== 'int') { + // only int ranges are possible at the moment + // @see https://psalm.dev/docs/annotating_code/type_syntax/scalar_types + continue; + } + $this->ranges[$varName] = [ + 'min' => $matches['rangeMin'][$index] === 'min' ? PHP_INT_MIN : (int)$matches['rangeMin'][$index], + 'max' => $matches['rangeMax'][$index] === 'max' ? PHP_INT_MAX : (int)$matches['rangeMax'][$index], + ]; + } } foreach ($reflection->getParameters() as $param) { @@ -106,6 +119,14 @@ class ControllerMethodReflector implements IControllerMethodReflector { return null; } + public function getRange(string $parameter): ?array { + if (array_key_exists($parameter, $this->ranges)) { + return $this->ranges[$parameter]; + } + + return null; + } + /** * @return array the arguments of the method with key => default value */ diff --git a/lib/private/AppFramework/Utility/SimpleContainer.php b/lib/private/AppFramework/Utility/SimpleContainer.php index 7aa5cb83926..83aed4381b3 100644 --- a/lib/private/AppFramework/Utility/SimpleContainer.php +++ b/lib/private/AppFramework/Utility/SimpleContainer.php @@ -37,8 +37,8 @@ use Pimple\Container; use Psr\Container\ContainerInterface; use ReflectionClass; use ReflectionException; -use ReflectionParameter; use ReflectionNamedType; +use ReflectionParameter; use function class_exists; /** @@ -105,6 +105,11 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer { try { return $this->query($resolveName); } catch (QueryException $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); } diff --git a/lib/private/AppFramework/Utility/TimeFactory.php b/lib/private/AppFramework/Utility/TimeFactory.php index 1e4655dd1cd..737777a11ac 100644 --- a/lib/private/AppFramework/Utility/TimeFactory.php +++ b/lib/private/AppFramework/Utility/TimeFactory.php @@ -34,7 +34,7 @@ use OCP\AppFramework\Utility\ITimeFactory; * Use this to get a timestamp or DateTime object in code to remain testable * * @since 8.0.0 - * @since 26.0.0 Extends the \Psr\Clock\ClockInterface interface + * @since 27.0.0 Implements the \Psr\Clock\ClockInterface interface * @ref https://www.php-fig.org/psr/psr-20/#21-clockinterface */ class TimeFactory implements ITimeFactory { @@ -73,4 +73,11 @@ class TimeFactory implements ITimeFactory { return $clone; } + + public function getTimeZone(?string $timezone = null): \DateTimeZone { + if ($timezone !== null) { + return new \DateTimeZone($timezone); + } + return $this->timezone; + } } |