diff options
Diffstat (limited to 'lib/public/AppFramework')
91 files changed, 7252 insertions, 0 deletions
diff --git a/lib/public/AppFramework/ApiController.php b/lib/public/AppFramework/ApiController.php new file mode 100644 index 00000000000..729582c8505 --- /dev/null +++ b/lib/public/AppFramework/ApiController.php @@ -0,0 +1,74 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCP\AppFramework; + +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\PublicPage; +use OCP\AppFramework\Http\Response; +use OCP\IRequest; + +/** + * Base class to inherit your controllers from that are used for RESTful APIs + * @since 7.0.0 + */ +abstract class ApiController extends Controller { + private $corsMethods; + private $corsAllowedHeaders; + private $corsMaxAge; + + /** + * constructor of the controller + * @param string $appName the name of the app + * @param IRequest $request an instance of the request + * @param string $corsMethods comma separated string of HTTP verbs which + * should be allowed for websites or webapps when calling your API, defaults to + * 'PUT, POST, GET, DELETE, PATCH' + * @param string $corsAllowedHeaders comma separated string of HTTP headers + * which should be allowed for websites or webapps when calling your API, + * defaults to 'Authorization, Content-Type, Accept' + * @param int $corsMaxAge number in seconds how long a preflighted OPTIONS + * request should be cached, defaults to 1728000 seconds + * @since 7.0.0 + */ + public function __construct($appName, + IRequest $request, + $corsMethods = 'PUT, POST, GET, DELETE, PATCH', + $corsAllowedHeaders = 'Authorization, Content-Type, Accept', + $corsMaxAge = 1728000) { + parent::__construct($appName, $request); + $this->corsMethods = $corsMethods; + $this->corsAllowedHeaders = $corsAllowedHeaders; + $this->corsMaxAge = $corsMaxAge; + } + + + /** + * This method implements a preflighted cors response for you that you can + * link to for the options request + * + * @since 7.0.0 + */ + #[NoCSRFRequired] + #[PublicPage] + #[NoAdminRequired] + public function preflightedCors() { + $origin = $this->request->getHeader('origin'); + if ($origin === '') { + $origin = '*'; + } + + $response = new Response(); + $response->addHeader('Access-Control-Allow-Origin', $origin); + $response->addHeader('Access-Control-Allow-Methods', $this->corsMethods); + $response->addHeader('Access-Control-Max-Age', (string)$this->corsMaxAge); + $response->addHeader('Access-Control-Allow-Headers', $this->corsAllowedHeaders); + $response->addHeader('Access-Control-Allow-Credentials', 'false'); + return $response; + } +} diff --git a/lib/public/AppFramework/App.php b/lib/public/AppFramework/App.php new file mode 100644 index 00000000000..c00fde47418 --- /dev/null +++ b/lib/public/AppFramework/App.php @@ -0,0 +1,140 @@ +<?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 OCP\AppFramework; + +use OC\AppFramework\Utility\SimpleContainer; +use OC\ServerContainer; +use OCP\IConfig; +use OCP\Server; +use Psr\Log\LoggerInterface; + +/** + * Class App + * + * Any application must inherit this call - all controller instances to be used are + * to be registered using IContainer::registerService + * @since 6.0.0 + */ +class App { + /** @var IAppContainer */ + private $container; + + /** + * Turns an app id into a namespace by convention. The id is split at the + * underscores, all parts are CamelCased and reassembled. e.g.: + * some_app_id -> OCA\SomeAppId + * @param string $appId the app id + * @param string $topNamespace the namespace which should be prepended to + * the transformed app id, defaults to OCA\ + * @return string the starting namespace for the app + * @since 8.0.0 + */ + public static function buildAppNamespace(string $appId, string $topNamespace = 'OCA\\'): string { + return \OC\AppFramework\App::buildAppNamespace($appId, $topNamespace); + } + + + /** + * @param string $appName + * @param array $urlParams an array with variables extracted from the routes + * @since 6.0.0 + */ + public function __construct(string $appName, array $urlParams = []) { + $runIsSetupDirectly = Server::get(IConfig::class)->getSystemValueBool('debug') + && !ini_get('zend.exception_ignore_args'); + + if ($runIsSetupDirectly) { + $applicationClassName = get_class($this); + $e = new \RuntimeException('App class ' . $applicationClassName . ' is not setup via query() but directly'); + $setUpViaQuery = false; + + $classNameParts = explode('\\', trim($applicationClassName, '\\')); + + foreach ($e->getTrace() as $step) { + if (isset($step['class'], $step['function'], $step['args'][0]) + && $step['class'] === ServerContainer::class + && $step['function'] === 'query' + && $step['args'][0] === $applicationClassName) { + $setUpViaQuery = true; + break; + } elseif (isset($step['class'], $step['function'], $step['args'][0]) + && $step['class'] === ServerContainer::class + && $step['function'] === 'getAppContainer' + && $step['args'][1] === $classNameParts[1]) { + $setUpViaQuery = true; + break; + } elseif (isset($step['class'], $step['function'], $step['args'][0]) + && $step['class'] === SimpleContainer::class + && preg_match('/{closure:OC\\\\AppFramework\\\\Utility\\\\SimpleContainer::buildClass\\(\\):\\d+}/', $step['function']) + && $step['args'][0] === $this) { + /* We are setup through a lazy ghost, fine */ + $setUpViaQuery = true; + break; + } + } + + if (!$setUpViaQuery && $applicationClassName !== \OCP\AppFramework\App::class) { + Server::get(LoggerInterface::class)->error($e->getMessage(), [ + 'app' => $appName, + 'exception' => $e, + ]); + } + } + + try { + $this->container = \OC::$server->getRegisteredAppContainer($appName); + } catch (QueryException $e) { + $this->container = new \OC\AppFramework\DependencyInjection\DIContainer($appName, $urlParams); + } + } + + /** + * @return IAppContainer + * @since 6.0.0 + */ + public function getContainer(): IAppContainer { + return $this->container; + } + + /** + * This function is called by the routing component to fire up the frameworks dispatch mechanism. + * + * Example code in routes.php of the task app: + * $this->create('tasks_index', '/')->get()->action( + * function($params){ + * $app = new TaskApp($params); + * $app->dispatch('PageController', 'index'); + * } + * ); + * + * + * Example for for TaskApp implementation: + * class TaskApp extends \OCP\AppFramework\App { + * + * public function __construct($params){ + * parent::__construct('tasks', $params); + * + * $this->getContainer()->registerService('PageController', function(IAppContainer $c){ + * $a = $c->query('API'); + * $r = $c->query('Request'); + * return new PageController($a, $r); + * }); + * } + * } + * + * @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 + * @since 6.0.0 + */ + public function dispatch(string $controllerName, string $methodName) { + \OC\AppFramework\App::main($controllerName, $methodName, $this->container); + } +} diff --git a/lib/public/AppFramework/Attribute/ASince.php b/lib/public/AppFramework/Attribute/ASince.php new file mode 100644 index 00000000000..1e0c45348cf --- /dev/null +++ b/lib/public/AppFramework/Attribute/ASince.php @@ -0,0 +1,34 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Attribute; + +use Attribute; + +/** + * Abstract base attribute to declare an API's stability. + * + * @since 32.0.0 + */ +#[Consumable(since: '32.0.0')] +abstract class ASince { + /** + * @param string $since For shipped apps and server code such as core/ and lib/, + * this should be the server version. For other apps it + * should be the semantic app version. + */ + public function __construct( + protected string $since, + ) { + } + + public function getSince(): string { + return $this->since; + } +} diff --git a/lib/public/AppFramework/Attribute/Catchable.php b/lib/public/AppFramework/Attribute/Catchable.php new file mode 100644 index 00000000000..d45401550f6 --- /dev/null +++ b/lib/public/AppFramework/Attribute/Catchable.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Attribute; + +use Attribute; + +/** + * Attribute to declare that the exception is "catchable" by apps. + * + * @since 32.0.0 + */ +#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)] +#[Consumable(since: '32.0.0')] +#[Implementable(since: '32.0.0')] +class Catchable extends ASince { +} diff --git a/lib/public/AppFramework/Attribute/Consumable.php b/lib/public/AppFramework/Attribute/Consumable.php new file mode 100644 index 00000000000..2175bb0af88 --- /dev/null +++ b/lib/public/AppFramework/Attribute/Consumable.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Attribute; + +use Attribute; + +/** + * Attribute to declare that the API stability is limited to "consuming" the + * class, interface, enum, etc. Apps are not allowed to implement or replace them. + * + * For events use @see \OCP\AppFramework\Attribute\Listenable + * For exceptions use @see \OCP\AppFramework\Attribute\Catchable + * + * @since 32.0.0 + */ +#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)] +#[Consumable(since: '32.0.0')] +#[Implementable(since: '32.0.0')] +class Consumable extends ASince { +} diff --git a/lib/public/AppFramework/Attribute/Dispatchable.php b/lib/public/AppFramework/Attribute/Dispatchable.php new file mode 100644 index 00000000000..ff703d4749e --- /dev/null +++ b/lib/public/AppFramework/Attribute/Dispatchable.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Attribute; + +use Attribute; + +/** + * Attribute to declare that the event is "dispatchable" by apps. + * + * @since 32.0.0 + */ +#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)] +#[Consumable(since: '32.0.0')] +#[Implementable(since: '32.0.0')] +class Dispatchable extends ASince { +} diff --git a/lib/public/AppFramework/Attribute/ExceptionalImplementable.php b/lib/public/AppFramework/Attribute/ExceptionalImplementable.php new file mode 100644 index 00000000000..23e9f830d9b --- /dev/null +++ b/lib/public/AppFramework/Attribute/ExceptionalImplementable.php @@ -0,0 +1,38 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Attribute; + +use Attribute; + +/** + * Attribute to declare that the API marked as Consumable/Listenable/Catchable + * has an exception and is Implementable/Dispatchable/Throwable by a dedicated + * app. Changes to such an API have to be communicated to the affected app maintainers. + * + * @since 32.0.0 + */ +#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)] +#[Consumable(since: '32.0.0')] +#[Implementable(since: '32.0.0')] +class ExceptionalImplementable { + public function __construct( + protected string $app, + protected ?string $class = null, + ) { + } + + public function getApp(): string { + return $this->app; + } + + public function getClass(): ?string { + return $this->class; + } +} diff --git a/lib/public/AppFramework/Attribute/Implementable.php b/lib/public/AppFramework/Attribute/Implementable.php new file mode 100644 index 00000000000..40ce0e0cf06 --- /dev/null +++ b/lib/public/AppFramework/Attribute/Implementable.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Attribute; + +use Attribute; + +/** + * Attribute to declare that the API stability is limited to "implementing" the + * class, interface, enum, etc. + * + * For events use @see \OCP\AppFramework\Attribute\Dispatchable + * For exceptions use @see \OCP\AppFramework\Attribute\Throwable + * + * @since 32.0.0 + */ +#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)] +#[Consumable(since: '32.0.0')] +#[Implementable(since: '32.0.0')] +class Implementable extends ASince { +} diff --git a/lib/public/AppFramework/Attribute/Listenable.php b/lib/public/AppFramework/Attribute/Listenable.php new file mode 100644 index 00000000000..98c2ca78690 --- /dev/null +++ b/lib/public/AppFramework/Attribute/Listenable.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Attribute; + +use Attribute; + +/** + * Attribute to declare that the event is "listenable" by apps. + * + * @since 32.0.0 + */ +#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)] +#[Consumable(since: '32.0.0')] +#[Implementable(since: '32.0.0')] +class Listenable extends ASince { +} diff --git a/lib/public/AppFramework/Attribute/Throwable.php b/lib/public/AppFramework/Attribute/Throwable.php new file mode 100644 index 00000000000..2c763c76b4c --- /dev/null +++ b/lib/public/AppFramework/Attribute/Throwable.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Attribute; + +use Attribute; + +/** + * Attribute to declare that the exception is "throwable" by apps. + * + * @since 32.0.0 + */ +#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)] +#[Consumable(since: '32.0.0')] +#[Implementable(since: '32.0.0')] +class Throwable extends ASince { +} diff --git a/lib/public/AppFramework/AuthPublicShareController.php b/lib/public/AppFramework/AuthPublicShareController.php new file mode 100644 index 00000000000..28a92fedcc9 --- /dev/null +++ b/lib/public/AppFramework/AuthPublicShareController.php @@ -0,0 +1,235 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework; + +use OCP\AppFramework\Http\Attribute\BruteForceProtection; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\PublicPage; +use OCP\AppFramework\Http\Attribute\UseSession; +use OCP\AppFramework\Http\RedirectResponse; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IRequest; +use OCP\ISession; +use OCP\IURLGenerator; + +/** + * Base controller for interactive public shares + * + * It will verify if the user is properly authenticated to the share. If not the + * user will be redirected to an authentication page. + * + * Use this for a controller that is to be called directly by a user. So the + * normal public share page for files/calendars etc. + * + * @since 14.0.0 + */ +abstract class AuthPublicShareController extends PublicShareController { + /** @var IURLGenerator */ + protected $urlGenerator; + + /** + * @since 14.0.0 + */ + public function __construct(string $appName, + IRequest $request, + ISession $session, + IURLGenerator $urlGenerator) { + parent::__construct($appName, $request, $session); + + $this->urlGenerator = $urlGenerator; + } + + /** + * Show the authentication page + * The form has to submit to the authenticate method route + * + * @since 14.0.0 + */ + #[NoCSRFRequired] + #[PublicPage] + public function showAuthenticate(): TemplateResponse { + return new TemplateResponse('core', 'publicshareauth', [], 'guest'); + } + + /** + * The template to show when authentication failed + * + * @since 14.0.0 + */ + protected function showAuthFailed(): TemplateResponse { + return new TemplateResponse('core', 'publicshareauth', ['wrongpw' => true], 'guest'); + } + + /** + * The template to show after user identification + * + * @since 24.0.0 + */ + protected function showIdentificationResult(bool $success): TemplateResponse { + return new TemplateResponse('core', 'publicshareauth', ['identityOk' => $success], 'guest'); + } + + /** + * Validates that the provided identity is allowed to receive a temporary password + * + * @since 24.0.0 + */ + protected function validateIdentity(?string $identityToken = null): bool { + return false; + } + + /** + * Generates a password + * + * @since 24.0.0 + */ + protected function generatePassword(): void { + } + + /** + * Verify the password + * + * @since 24.0.0 + */ + protected function verifyPassword(string $password): bool { + return false; + } + + /** + * Function called after failed authentication + * + * You can use this to do some logging for example + * + * @since 14.0.0 + */ + protected function authFailed() { + } + + /** + * Function called after successful authentication + * + * You can use this to do some logging for example + * + * @since 14.0.0 + */ + protected function authSucceeded() { + } + + /** + * Authenticate the share + * + * @since 14.0.0 + */ + #[BruteForceProtection(action: 'publicLinkAuth')] + #[PublicPage] + #[UseSession] + final public function authenticate(string $password = '', string $passwordRequest = 'no', string $identityToken = '') { + // Already authenticated + if ($this->isAuthenticated()) { + return $this->getRedirect(); + } + + // Is user requesting a temporary password? + if ($passwordRequest == '') { + if ($this->validateIdentity($identityToken)) { + $this->generatePassword(); + $response = $this->showIdentificationResult(true); + return $response; + } else { + $response = $this->showIdentificationResult(false); + $response->throttle(); + return $response; + } + } + + if (!$this->verifyPassword($password)) { + $this->authFailed(); + $response = $this->showAuthFailed(); + $response->throttle(); + return $response; + } + + $this->session->regenerateId(true, true); + $response = $this->getRedirect(); + + $this->session->set('public_link_authenticated_token', $this->getToken()); + $this->session->set('public_link_authenticated_password_hash', $this->getPasswordHash()); + + $this->authSucceeded(); + + return $response; + } + + /** + * Default landing page + * + * @since 14.0.0 + */ + abstract public function showShare(): TemplateResponse; + + /** + * @since 14.0.0 + */ + final public function getAuthenticationRedirect(string $redirect): RedirectResponse { + return new RedirectResponse( + $this->urlGenerator->linkToRoute($this->getRoute('showAuthenticate'), ['token' => $this->getToken(), 'redirect' => $redirect]) + ); + } + + + /** + * @since 14.0.0 + */ + private function getRoute(string $function): string { + $app = strtolower($this->appName); + $class = (new \ReflectionClass($this))->getShortName(); + if (str_ends_with($class, 'Controller')) { + $class = substr($class, 0, -10); + } + return $app . '.' . $class . '.' . $function; + } + + /** + * @since 14.0.0 + */ + private function getRedirect(): RedirectResponse { + //Get all the stored redirect parameters: + $params = $this->session->get('public_link_authenticate_redirect'); + + $route = $this->getRoute('showShare'); + + if ($params === null) { + $params = [ + 'token' => $this->getToken(), + ]; + } else { + $params = json_decode($params, true); + if (isset($params['_route'])) { + $route = $params['_route']; + unset($params['_route']); + } + + // If the token doesn't match the rest of the arguments can't be trusted either + if (isset($params['token']) && $params['token'] !== $this->getToken()) { + $params = [ + 'token' => $this->getToken(), + ]; + } + + // We need a token + if (!isset($params['token'])) { + $params = [ + 'token' => $this->getToken(), + ]; + } + } + + return new RedirectResponse($this->urlGenerator->linkToRoute($route, $params)); + } +} diff --git a/lib/public/AppFramework/Bootstrap/IBootContext.php b/lib/public/AppFramework/Bootstrap/IBootContext.php new file mode 100644 index 00000000000..cdf3a0af732 --- /dev/null +++ b/lib/public/AppFramework/Bootstrap/IBootContext.php @@ -0,0 +1,59 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Bootstrap; + +use OCP\AppFramework\IAppContainer; +use OCP\IServerContainer; +use Psr\Container\ContainerExceptionInterface; +use Throwable; + +/** + * @since 20.0.0 + */ +interface IBootContext { + /** + * Get hold of the app's container + * + * Useful to register and query app-specific services + * + * @return IAppContainer + * @since 20.0.0 + */ + public function getAppContainer(): IAppContainer; + + /** + * Get hold of the server DI container + * + * Useful to register and query system-wide services + * + * @return IServerContainer + * @since 20.0.0 + */ + public function getServerContainer(): IServerContainer; + + /** + * Invoke the given callable and inject all parameters based on their types + * and names + * + * Note: when used with methods, make sure they are public or use \Closure::fromCallable + * to wrap the private method call, e.g. + * * `$context->injectFn([$obj, 'publicMethod'])` + * * `$context->injectFn([$this, 'publicMethod'])` + * * `$context->injectFn(\Closure::fromCallable([$this, 'privateMethod']))` + * + * Note: the app container will be queried + * + * @param callable $fn + * @throws ContainerExceptionInterface if at least one of the parameter can't be resolved + * @throws Throwable any error the function invocation might cause + * @return mixed|null the return value of the invoked function, if any + * @since 20.0.0 + */ + public function injectFn(callable $fn); +} diff --git a/lib/public/AppFramework/Bootstrap/IBootstrap.php b/lib/public/AppFramework/Bootstrap/IBootstrap.php new file mode 100644 index 00000000000..7260d2b77a1 --- /dev/null +++ b/lib/public/AppFramework/Bootstrap/IBootstrap.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Bootstrap; + +/** + * @since 20.0.0 + */ +interface IBootstrap { + /** + * @param IRegistrationContext $context + * + * @since 20.0.0 + */ + public function register(IRegistrationContext $context): void; + + /** + * Boot the application + * + * At this stage you can assume that all services are registered and the DI + * container(s) are ready to be queried. + * + * @param IBootContext $context + * + * @since 20.0.0 + */ + public function boot(IBootContext $context): void; +} diff --git a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php new file mode 100644 index 00000000000..70b35228c87 --- /dev/null +++ b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php @@ -0,0 +1,450 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Bootstrap; + +use OCP\AppFramework\IAppContainer; +use OCP\Authentication\TwoFactorAuth\IProvider; +use OCP\Calendar\ICalendarProvider; +use OCP\Capabilities\ICapability; +use OCP\Collaboration\Reference\IReferenceProvider; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Template\ICustomTemplateProvider; +use OCP\IContainer; +use OCP\Mail\Provider\IProvider as IMailProvider; +use OCP\Notification\INotifier; +use OCP\Preview\IProviderV2; +use OCP\SpeechToText\ISpeechToTextProvider; +use OCP\TextProcessing\IProvider as ITextProcessingProvider; +use OCP\TextToImage\IProvider as ITextToImageProvider; +use OCP\Translation\ITranslationProvider; + +/** + * The context object passed to IBootstrap::register + * + * @since 20.0.0 + * @see IBootstrap::register() + */ +interface IRegistrationContext { + /** + * @param string $capability + * @psalm-param class-string<ICapability> $capability + * @see IAppContainer::registerCapability + * + * @since 20.0.0 + */ + public function registerCapability(string $capability): void; + + /** + * Register an implementation of \OCP\Support\CrashReport\IReporter that + * will receive unhandled exceptions and throwables + * + * @param string $reporterClass + * @psalm-param class-string<\OCP\Support\CrashReport\IReporter> $reporterClass + * @return void + * @since 20.0.0 + */ + public function registerCrashReporter(string $reporterClass): void; + + /** + * Register an implementation of \OCP\Dashboard\IWidget that + * will handle the implementation of a dashboard widget + * + * @param string $widgetClass + * @psalm-param class-string<\OCP\Dashboard\IWidget> $widgetClass + * @return void + * @since 20.0.0 + */ + public function registerDashboardWidget(string $widgetClass): void; + + /** + * Register a service + * + * @param string $name + * @param callable $factory + * @psalm-param callable(\Psr\Container\ContainerInterface): mixed $factory + * @param bool $shared If set to true the factory result will be cached otherwise every query will call the factory again + * + * @return void + * @see IContainer::registerService() + * + * @since 20.0.0 + */ + public function registerService(string $name, callable $factory, bool $shared = true): void; + + /** + * @param string $alias + * @psalm-param string|class-string $alias + * @param string $target + * @psalm-param string|class-string $target + * + * @return void + * @see IContainer::registerAlias() + * + * @since 20.0.0 + */ + public function registerServiceAlias(string $alias, string $target): void; + + /** + * @param string $name + * @param mixed $value + * + * @return void + * @see IContainer::registerParameter() + * + * @since 20.0.0 + */ + public function registerParameter(string $name, $value): void; + + /** + * Register a service listener + * + * This is equivalent to calling IEventDispatcher::addServiceListener + * + * @psalm-template T of \OCP\EventDispatcher\Event + * @param string $event preferably the fully-qualified class name of the Event sub class to listen for + * @psalm-param string|class-string<T> $event preferably the fully-qualified class name of the Event sub class to listen for + * @param string $listener fully qualified class name (or ::class notation) of a \OCP\EventDispatcher\IEventListener that can be built by the DI container + * @psalm-param class-string<\OCP\EventDispatcher\IEventListener<T>> $listener fully qualified class name that can be built by the DI container + * @param int $priority The higher this value, the earlier an event + * listener will be triggered in the chain (defaults to 0) + * + * @see IEventDispatcher::addServiceListener() + * + * @since 20.0.0 + */ + public function registerEventListener(string $event, string $listener, int $priority = 0): void; + + /** + * @param string $class + * @param bool $global load this middleware also for requests of other apps? Added in Nextcloud 26 + * @psalm-param class-string<\OCP\AppFramework\Middleware> $class + * + * @return void + * @see IAppContainer::registerMiddleWare() + * + * @since 20.0.0 + * @since 26.0.0 Added optional argument $global + */ + public function registerMiddleware(string $class, bool $global = false): void; + + /** + * Register a search provider for the unified search + * + * It is allowed to register more than one provider per app as the search + * results can go into distinct sections, e.g. "Files" and "Files shared + * with you" in the Files app. + * + * @param string $class + * @psalm-param class-string<\OCP\Search\IProvider> $class + * + * @return void + * + * @since 20.0.0 + */ + public function registerSearchProvider(string $class): void; + + /** + * Register an alternative login option + * + * It is allowed to register more than one option per app. + * + * @param string $class + * @psalm-param class-string<\OCP\Authentication\IAlternativeLogin> $class + * + * @return void + * + * @since 20.0.0 + */ + public function registerAlternativeLogin(string $class): void; + + /** + * Register an initialstate provider + * + * It is allowed to register more than one provider per app. + * + * @param string $class + * @psalm-param class-string<\OCP\AppFramework\Services\InitialStateProvider> $class + * + * @return void + * + * @since 21.0.0 + */ + public function registerInitialStateProvider(string $class): void; + + /** + * Register a well known protocol handler + * + * It is allowed to register more than one handler per app. + * + * @param string $class + * @psalm-param class-string<\OCP\Http\WellKnown\IHandler> $class + * + * @return void + * + * @since 21.0.0 + */ + public function registerWellKnownHandler(string $class): void; + + /** + * Register a custom SpeechToText provider class that can provide transcription + * of audio through the OCP\SpeechToText APIs + * + * @param string $providerClass + * @psalm-param class-string<ISpeechToTextProvider> $providerClass + * @since 27.0.0 + */ + public function registerSpeechToTextProvider(string $providerClass): void; + + /** + * Register a custom text processing provider class that provides a promptable language model + * through the OCP\TextProcessing APIs + * + * @param string $providerClass + * @psalm-param class-string<ITextProcessingProvider> $providerClass + * @since 27.1.0 + */ + public function registerTextProcessingProvider(string $providerClass): void; + + /** + * Register a custom text2image provider class that provides the possibility to generate images + * through the OCP\TextToImage APIs + * + * @param string $providerClass + * @psalm-param class-string<ITextToImageProvider> $providerClass + * @since 28.0.0 + */ + public function registerTextToImageProvider(string $providerClass): void; + + /** + * Register a custom template provider class that is able to inject custom templates + * in addition to the user defined ones + * + * @param string $providerClass + * @psalm-param class-string<ICustomTemplateProvider> $providerClass + * @since 21.0.0 + */ + public function registerTemplateProvider(string $providerClass): void; + + /** + * Register a custom translation provider class that can provide translation + * between languages through the OCP\Translation APIs + * + * @param string $providerClass + * @psalm-param class-string<ITranslationProvider> $providerClass + * @since 21.0.0 + */ + public function registerTranslationProvider(string $providerClass): void; + + /** + * Register an INotifier class + * + * @param string $notifierClass + * @psalm-param class-string<INotifier> $notifierClass + * @since 22.0.0 + */ + public function registerNotifierService(string $notifierClass): void; + + /** + * Register a two-factor provider + * + * @param string $twoFactorProviderClass + * @psalm-param class-string<IProvider> $twoFactorProviderClass + * @since 22.0.0 + */ + public function registerTwoFactorProvider(string $twoFactorProviderClass): void; + + /** + * Register a preview provider + * + * It is allowed to register more than one provider per app. + * + * @param string $previewProviderClass + * @param string $mimeTypeRegex + * @psalm-param class-string<IProviderV2> $previewProviderClass + * @since 23.0.0 + */ + public function registerPreviewProvider(string $previewProviderClass, string $mimeTypeRegex): void; + + /** + * Register a calendar provider + * + * @param string $class + * @psalm-param class-string<ICalendarProvider> $class + * @since 23.0.0 + */ + public function registerCalendarProvider(string $class): void; + + /** + * Register a reference provider + * + * @param string $class + * @psalm-param class-string<IReferenceProvider> $class + * @since 25.0.0 + */ + public function registerReferenceProvider(string $class): void; + + /** + * Register an implementation of \OCP\Profile\ILinkAction that + * will handle the implementation of a profile link action + * + * @param string $actionClass + * @psalm-param class-string<\OCP\Profile\ILinkAction> $actionClass + * @return void + * @since 23.0.0 + */ + public function registerProfileLinkAction(string $actionClass): void; + + /** + * Register the backend of the Talk app + * + * This service must only be used by the Talk app + * + * @param string $backend + * @return void + * @since 24.0.0 + */ + public function registerTalkBackend(string $backend): void; + + /** + * Register a resource backend for the DAV server + * + * @param string $actionClass + * @psalm-param class-string<\OCP\Calendar\Resource\IBackend> $actionClass + * @return void + * @since 24.0.0 + */ + public function registerCalendarResourceBackend(string $class): void; + + /** + * Register a room backend for the DAV server + * + * @param string $actionClass + * @psalm-param class-string<\OCP\Calendar\Room\IBackend> $actionClass + * @return void + * @since 24.0.0 + */ + public function registerCalendarRoomBackend(string $class): void; + + /** + * @param string $class + * @psalm-param class-string<\OCP\Calendar\Room\IBackend> $actionClass + * @return void + * @since 29.0.0 + */ + public function registerTeamResourceProvider(string $class): void; + + /** + * Register an implementation of \OCP\UserMigration\IMigrator that + * will handle the implementation of a migrator + * + * @param string $migratorClass + * @psalm-param class-string<\OCP\UserMigration\IMigrator> $migratorClass + * @return void + * @since 24.0.0 + */ + public function registerUserMigrator(string $migratorClass): void; + + /** + * Announce methods of classes that may contain sensitive values, which + * should be obfuscated before being logged. + * + * @param string $class + * @param string[] $methods + * @return void + * @since 25.0.0 + */ + public function registerSensitiveMethods(string $class, array $methods): void; + + /** + * Register an implementation of IPublicShareTemplateProvider. + * + * @param string $class + * @psalm-param class-string<\OCP\Share\IPublicShareTemplateProvider> $class + * @return void + * @since 26.0.0 + */ + public function registerPublicShareTemplateProvider(string $class): void; + + /** + * Register an implementation of \OCP\SetupCheck\ISetupCheck that + * will handle the implementation of a setup check + * + * @param class-string<\OCP\SetupCheck\ISetupCheck> $setupCheckClass + * @since 28.0.0 + */ + public function registerSetupCheck(string $setupCheckClass): void; + + /** + * Register an implementation of \OCP\Settings\IDeclarativeSettings that + * will handle the implementation of declarative settings + * + * @param string $declarativeSettingsClass + * @psalm-param class-string<\OCP\Settings\IDeclarativeSettingsForm> $declarativeSettingsClass + * @return void + * @since 29.0.0 + */ + public function registerDeclarativeSettings(string $declarativeSettingsClass): void; + + /** + * Register an implementation of \OCP\TaskProcessing\IProvider that + * will handle the implementation of task processing + * + * @param string $taskProcessingProviderClass + * @psalm-param class-string<\OCP\TaskProcessing\IProvider> $taskProcessingProviderClass + * @return void + * @since 30.0.0 + */ + public function registerTaskProcessingProvider(string $taskProcessingProviderClass): void; + + /** + * Register an implementation of \OCP\TaskProcessing\ITaskType that + * will handle the implementation of a task processing type + * + * @param string $taskProcessingTaskTypeClass + * @psalm-param class-string<\OCP\TaskProcessing\ITaskType> $taskProcessingTaskTypeClass + * @return void + * @since 30.0.0 + */ + public function registerTaskProcessingTaskType(string $taskProcessingTaskTypeClass): void; + + /** + * Register an implementation of \OCP\Files\Conversion\IConversionProvider + * that will handle the conversion of files from one MIME type to another + * + * @param string $class + * @psalm-param class-string<\OCP\Files\Conversion\IConversionProvider> $class + * + * @return void + * + * @since 31.0.0 + */ + public function registerFileConversionProvider(string $class): void; + + /** + * Register a mail provider + * + * @param string $class + * @psalm-param class-string<IMailProvider> $class + * @since 30.0.0 + */ + public function registerMailProvider(string $class): void; + + + /** + * Register an implementation of \OCP\Config\Lexicon\IConfigLexicon that + * will handle the config lexicon + * + * @param string $configLexiconClass + * + * @psalm-param class-string<\OCP\Config\Lexicon\ILexicon> $configLexiconClass + * @since 31.0.0 + */ + public function registerConfigLexicon(string $configLexiconClass): void; +} diff --git a/lib/public/AppFramework/Controller.php b/lib/public/AppFramework/Controller.php new file mode 100644 index 00000000000..cdeaac99366 --- /dev/null +++ b/lib/public/AppFramework/Controller.php @@ -0,0 +1,141 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCP\AppFramework; + +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http\Response; +use OCP\IRequest; + +/** + * Base class to inherit your controllers from + * @since 6.0.0 + */ +abstract class Controller { + /** + * app name + * @var string + * @since 7.0.0 + */ + protected $appName; + + /** + * current request + * @var \OCP\IRequest + * @since 6.0.0 + */ + protected $request; + + /** + * @var array + * @since 7.0.0 + */ + private $responders; + + /** + * constructor of the controller + * @param string $appName the name of the app + * @param IRequest $request an instance of the request + * @since 6.0.0 - parameter $appName was added in 7.0.0 - parameter $app was removed in 7.0.0 + */ + public function __construct($appName, + IRequest $request) { + $this->appName = $appName; + $this->request = $request; + + // default responders + $this->responders = [ + 'json' => function ($data) { + if ($data instanceof DataResponse) { + $response = new JSONResponse( + $data->getData(), + $data->getStatus() + ); + $dataHeaders = $data->getHeaders(); + $headers = $response->getHeaders(); + // do not overwrite Content-Type if it already exists + if (isset($dataHeaders['Content-Type'])) { + unset($headers['Content-Type']); + } + $response->setHeaders(array_merge($dataHeaders, $headers)); + + if ($data->getETag() !== null) { + $response->setETag($data->getETag()); + } + if ($data->getLastModified() !== null) { + $response->setLastModified($data->getLastModified()); + } + if ($data->isThrottled()) { + $response->throttle($data->getThrottleMetadata()); + } + + return $response; + } + return new JSONResponse($data); + } + ]; + } + + + /** + * Parses an HTTP accept header and returns the supported responder type + * @param string $acceptHeader + * @param string $default + * @return string the responder type + * @since 7.0.0 + * @since 9.1.0 Added default parameter + */ + public function getResponderByHTTPHeader($acceptHeader, $default = 'json') { + $headers = explode(',', $acceptHeader); + + // return the first matching responder + foreach ($headers as $header) { + $header = strtolower(trim($header)); + + $responder = str_replace('application/', '', $header); + + if (array_key_exists($responder, $this->responders)) { + return $responder; + } + } + + // no matching header return default + return $default; + } + + + /** + * Registers a formatter for a type + * @param string $format + * @param \Closure $responder + * @since 7.0.0 + */ + protected function registerResponder($format, \Closure $responder) { + $this->responders[$format] = $responder; + } + + + /** + * Serializes and formats a response + * @param mixed $response the value that was returned from a controller and + * is not a Response instance + * @param string $format the format for which a formatter has been registered + * @throws \DomainException if format does not match a registered formatter + * @return Response + * @since 7.0.0 + */ + public function buildResponse($response, $format = 'json') { + if (array_key_exists($format, $this->responders)) { + $responder = $this->responders[$format]; + + return $responder($response); + } + throw new \DomainException('No responder registered for format ' + . $format . '!'); + } +} diff --git a/lib/public/AppFramework/Db/DoesNotExistException.php b/lib/public/AppFramework/Db/DoesNotExistException.php new file mode 100644 index 00000000000..416268b27c1 --- /dev/null +++ b/lib/public/AppFramework/Db/DoesNotExistException.php @@ -0,0 +1,25 @@ +<?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 OCP\AppFramework\Db; + +/** + * This is returned or should be returned when a find request does not find an + * entry in the database + * @since 7.0.0 + */ +class DoesNotExistException extends \Exception implements IMapperException { + /** + * Constructor + * @param string $msg the error message + * @since 7.0.0 + */ + public function __construct($msg) { + parent::__construct($msg); + } +} diff --git a/lib/public/AppFramework/Db/Entity.php b/lib/public/AppFramework/Db/Entity.php new file mode 100644 index 00000000000..3094070af5f --- /dev/null +++ b/lib/public/AppFramework/Db/Entity.php @@ -0,0 +1,314 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCP\AppFramework\Db; + +use OCP\DB\Types; + +use function lcfirst; +use function substr; + +/** + * @method int getId() + * @method void setId(int $id) + * @since 7.0.0 + * @psalm-consistent-constructor + */ +abstract class Entity { + /** + * @var int + */ + public $id; + + private array $_updatedFields = []; + /** @var array<string, \OCP\DB\Types::*> */ + private array $_fieldTypes = ['id' => 'integer']; + + /** + * Simple alternative constructor for building entities from a request + * @param array $params the array which was obtained via $this->params('key') + * in the controller + * @since 7.0.0 + */ + public static function fromParams(array $params): static { + $instance = new static(); + + foreach ($params as $key => $value) { + $method = 'set' . ucfirst($key); + $instance->$method($value); + } + + return $instance; + } + + + /** + * Maps the keys of the row array to the attributes + * @param array $row the row to map onto the entity + * @since 7.0.0 + */ + public static function fromRow(array $row): static { + $instance = new static(); + + foreach ($row as $key => $value) { + $prop = $instance->columnToProperty($key); + $instance->setter($prop, [$value]); + } + + $instance->resetUpdatedFields(); + + return $instance; + } + + + /** + * @return array<string, \OCP\DB\Types::*> with attribute and type + * @since 7.0.0 + */ + public function getFieldTypes(): array { + return $this->_fieldTypes; + } + + + /** + * Marks the entity as clean needed for setting the id after the insertion + * @since 7.0.0 + */ + public function resetUpdatedFields(): void { + $this->_updatedFields = []; + } + + /** + * Generic setter for properties + * + * @throws \InvalidArgumentException + * @since 7.0.0 + * + */ + protected function setter(string $name, array $args): void { + // setters should only work for existing attributes + if (!property_exists($this, $name)) { + throw new \BadFunctionCallException($name . ' is not a valid attribute'); + } + + if ($args[0] === $this->$name) { + return; + } + $this->markFieldUpdated($name); + + // if type definition exists, cast to correct type + if ($args[0] !== null && array_key_exists($name, $this->_fieldTypes)) { + $type = $this->_fieldTypes[$name]; + if ($type === Types::BLOB) { + // (B)LOB is treated as string when we read from the DB + if (is_resource($args[0])) { + $args[0] = stream_get_contents($args[0]); + } + $type = Types::STRING; + } + + switch ($type) { + case Types::BIGINT: + case Types::SMALLINT: + settype($args[0], Types::INTEGER); + break; + case Types::BINARY: + case Types::DECIMAL: + case Types::TEXT: + settype($args[0], Types::STRING); + break; + case Types::TIME: + case Types::DATE: + case Types::DATETIME: + case Types::DATETIME_TZ: + if (!$args[0] instanceof \DateTime) { + $args[0] = new \DateTime($args[0]); + } + break; + case Types::TIME_IMMUTABLE: + case Types::DATE_IMMUTABLE: + case Types::DATETIME_IMMUTABLE: + case Types::DATETIME_TZ_IMMUTABLE: + if (!$args[0] instanceof \DateTimeImmutable) { + $args[0] = new \DateTimeImmutable($args[0]); + } + break; + case Types::JSON: + if (!is_array($args[0])) { + $args[0] = json_decode($args[0], true); + } + break; + default: + settype($args[0], $type); + } + } + $this->$name = $args[0]; + + } + + /** + * Generic getter for properties + * @since 7.0.0 + */ + protected function getter(string $name): mixed { + // getters should only work for existing attributes + if (property_exists($this, $name)) { + return $this->$name; + } else { + throw new \BadFunctionCallException($name + . ' is not a valid attribute'); + } + } + + + /** + * Each time a setter is called, push the part after set + * into an array: for instance setId will save Id in the + * updated fields array so it can be easily used to create the + * getter method + * @since 7.0.0 + */ + public function __call(string $methodName, array $args) { + if (str_starts_with($methodName, 'set')) { + $this->setter(lcfirst(substr($methodName, 3)), $args); + } elseif (str_starts_with($methodName, 'get')) { + return $this->getter(lcfirst(substr($methodName, 3))); + } elseif ($this->isGetterForBoolProperty($methodName)) { + return $this->getter(lcfirst(substr($methodName, 2))); + } else { + throw new \BadFunctionCallException($methodName + . ' does not exist'); + } + } + + /** + * @param string $methodName + * @return bool + * @since 18.0.0 + */ + protected function isGetterForBoolProperty(string $methodName): bool { + if (str_starts_with($methodName, 'is')) { + $fieldName = lcfirst(substr($methodName, 2)); + return isset($this->_fieldTypes[$fieldName]) && str_starts_with($this->_fieldTypes[$fieldName], 'bool'); + } + return false; + } + + /** + * Mark am attribute as updated + * @param string $attribute the name of the attribute + * @since 7.0.0 + */ + protected function markFieldUpdated(string $attribute): void { + $this->_updatedFields[$attribute] = true; + } + + + /** + * Transform a database columnname to a property + * + * @param string $columnName the name of the column + * @return string the property name + * @since 7.0.0 + */ + public function columnToProperty(string $columnName) { + $parts = explode('_', $columnName); + $property = ''; + + foreach ($parts as $part) { + if ($property === '') { + $property = $part; + } else { + $property .= ucfirst($part); + } + } + + return $property; + } + + + /** + * Transform a property to a database column name + * + * @param string $property the name of the property + * @return string the column name + * @since 7.0.0 + */ + public function propertyToColumn(string $property): string { + $parts = preg_split('/(?=[A-Z])/', $property); + + $column = ''; + foreach ($parts as $part) { + if ($column === '') { + $column = $part; + } else { + $column .= '_' . lcfirst($part); + } + } + + return $column; + } + + + /** + * @return array array of updated fields for update query + * @since 7.0.0 + */ + public function getUpdatedFields(): array { + return $this->_updatedFields; + } + + + /** + * Adds type information for a field so that it's automatically cast to + * that value once its being returned from the database + * + * @param string $fieldName the name of the attribute + * @param \OCP\DB\Types::* $type the type which will be used to match a cast + * @since 31.0.0 Parameter $type is now restricted to {@see \OCP\DB\Types} constants. The formerly accidentally supported types 'int'|'bool'|'double' are mapped to Types::INTEGER|Types::BOOLEAN|Types::FLOAT accordingly. + * @since 7.0.0 + */ + protected function addType(string $fieldName, string $type): void { + /** @psalm-suppress TypeDoesNotContainType */ + if (in_array($type, ['bool', 'double', 'int', 'array', 'object'], true)) { + // Mapping legacy strings to the actual types + $type = match ($type) { + 'int' => Types::INTEGER, + 'bool' => Types::BOOLEAN, + 'double' => Types::FLOAT, + 'array', + 'object' => Types::STRING, + }; + } + + $this->_fieldTypes[$fieldName] = $type; + } + + + /** + * Slugify the value of a given attribute + * Warning: This doesn't result in a unique value + * + * @param string $attributeName the name of the attribute, which value should be slugified + * @return string slugified value + * @since 7.0.0 + * @deprecated 24.0.0 + */ + public function slugify(string $attributeName): string { + // toSlug should only work for existing attributes + if (property_exists($this, $attributeName)) { + $value = $this->$attributeName; + // replace everything except alphanumeric with a single '-' + $value = preg_replace('/[^A-Za-z0-9]+/', '-', $value); + $value = strtolower($value); + // trim '-' + return trim($value, '-'); + } + + throw new \BadFunctionCallException($attributeName . ' is not a valid attribute'); + } +} diff --git a/lib/public/AppFramework/Db/IMapperException.php b/lib/public/AppFramework/Db/IMapperException.php new file mode 100644 index 00000000000..3e91422a89f --- /dev/null +++ b/lib/public/AppFramework/Db/IMapperException.php @@ -0,0 +1,14 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Db; + +/** + * @since 16.0.0 + */ +interface IMapperException extends \Throwable { +} diff --git a/lib/public/AppFramework/Db/MultipleObjectsReturnedException.php b/lib/public/AppFramework/Db/MultipleObjectsReturnedException.php new file mode 100644 index 00000000000..e83bc1647d7 --- /dev/null +++ b/lib/public/AppFramework/Db/MultipleObjectsReturnedException.php @@ -0,0 +1,25 @@ +<?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 OCP\AppFramework\Db; + +/** + * This is returned or should be returned when a find request finds more than one + * row + * @since 7.0.0 + */ +class MultipleObjectsReturnedException extends \Exception implements IMapperException { + /** + * Constructor + * @param string $msg the error message + * @since 7.0.0 + */ + public function __construct($msg) { + parent::__construct($msg); + } +} diff --git a/lib/public/AppFramework/Db/QBMapper.php b/lib/public/AppFramework/Db/QBMapper.php new file mode 100644 index 00000000000..7fb5b2a9afd --- /dev/null +++ b/lib/public/AppFramework/Db/QBMapper.php @@ -0,0 +1,377 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Db; + +use Generator; +use OCP\DB\Exception; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\DB\Types; +use OCP\IDBConnection; + +/** + * Simple parent class for inheriting your data access layer from. This class + * may be subject to change in the future + * + * @since 14.0.0 + * + * @template T of Entity + */ +abstract class QBMapper { + /** @var string */ + protected $tableName; + + /** @var string|class-string<T> */ + protected $entityClass; + + /** @var IDBConnection */ + protected $db; + + /** + * @param IDBConnection $db Instance of the Db abstraction layer + * @param string $tableName the name of the table. set this to allow entity + * @param class-string<T>|null $entityClass the name of the entity that the sql should be + * mapped to queries without using sql + * @since 14.0.0 + */ + public function __construct(IDBConnection $db, string $tableName, ?string $entityClass = null) { + $this->db = $db; + $this->tableName = $tableName; + + // if not given set the entity name to the class without the mapper part + // cache it here for later use since reflection is slow + if ($entityClass === null) { + $this->entityClass = str_replace('Mapper', '', \get_class($this)); + } else { + $this->entityClass = $entityClass; + } + } + + + /** + * @return string the table name + * @since 14.0.0 + */ + public function getTableName(): string { + return $this->tableName; + } + + + /** + * Deletes an entity from the table + * + * @param Entity $entity the entity that should be deleted + * @psalm-param T $entity the entity that should be deleted + * @return Entity the deleted entity + * @psalm-return T the deleted entity + * @throws Exception + * @since 14.0.0 + */ + public function delete(Entity $entity): Entity { + $qb = $this->db->getQueryBuilder(); + + $idType = $this->getParameterTypeForProperty($entity, 'id'); + + $qb->delete($this->tableName) + ->where( + $qb->expr()->eq('id', $qb->createNamedParameter($entity->getId(), $idType)) + ); + $qb->executeStatement(); + return $entity; + } + + + /** + * Creates a new entry in the db from an entity + * + * @param Entity $entity the entity that should be created + * @psalm-param T $entity the entity that should be created + * @return Entity the saved entity with the set id + * @psalm-return T the saved entity with the set id + * @throws Exception + * @since 14.0.0 + */ + public function insert(Entity $entity): Entity { + // get updated fields to save, fields have to be set using a setter to + // be saved + $properties = $entity->getUpdatedFields(); + + $qb = $this->db->getQueryBuilder(); + $qb->insert($this->tableName); + + // build the fields + foreach ($properties as $property => $updated) { + $column = $entity->propertyToColumn($property); + $getter = 'get' . ucfirst($property); + $value = $entity->$getter(); + + $type = $this->getParameterTypeForProperty($entity, $property); + $qb->setValue($column, $qb->createNamedParameter($value, $type)); + } + + $qb->executeStatement(); + + if ($entity->id === null) { + // When autoincrement is used id is always an int + $entity->setId($qb->getLastInsertId()); + } + + return $entity; + } + + /** + * Tries to creates a new entry in the db from an entity and + * updates an existing entry if duplicate keys are detected + * by the database + * + * @param Entity $entity the entity that should be created/updated + * @psalm-param T $entity the entity that should be created/updated + * @return Entity the saved entity with the (new) id + * @psalm-return T the saved entity with the (new) id + * @throws Exception + * @throws \InvalidArgumentException if entity has no id + * @since 15.0.0 + */ + public function insertOrUpdate(Entity $entity): Entity { + try { + return $this->insert($entity); + } catch (Exception $ex) { + if ($ex->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { + return $this->update($entity); + } + throw $ex; + } + } + + /** + * Updates an entry in the db from an entity + * + * @param Entity $entity the entity that should be created + * @psalm-param T $entity the entity that should be created + * @return Entity the saved entity with the set id + * @psalm-return T the saved entity with the set id + * @throws Exception + * @throws \InvalidArgumentException if entity has no id + * @since 14.0.0 + */ + public function update(Entity $entity): Entity { + // if entity wasn't changed it makes no sense to run a db query + $properties = $entity->getUpdatedFields(); + if (\count($properties) === 0) { + return $entity; + } + + // entity needs an id + $id = $entity->getId(); + if ($id === null) { + throw new \InvalidArgumentException( + 'Entity which should be updated has no id'); + } + + // get updated fields to save, fields have to be set using a setter to + // be saved + // do not update the id field + unset($properties['id']); + + $qb = $this->db->getQueryBuilder(); + $qb->update($this->tableName); + + // build the fields + foreach ($properties as $property => $updated) { + $column = $entity->propertyToColumn($property); + $getter = 'get' . ucfirst($property); + $value = $entity->$getter(); + + $type = $this->getParameterTypeForProperty($entity, $property); + $qb->set($column, $qb->createNamedParameter($value, $type)); + } + + $idType = $this->getParameterTypeForProperty($entity, 'id'); + + $qb->where( + $qb->expr()->eq('id', $qb->createNamedParameter($id, $idType)) + ); + $qb->executeStatement(); + + return $entity; + } + + /** + * Returns the type parameter for the QueryBuilder for a specific property + * of the $entity + * + * @param Entity $entity The entity to get the types from + * @psalm-param T $entity + * @param string $property The property of $entity to get the type for + * @return int|string + * @since 16.0.0 + */ + protected function getParameterTypeForProperty(Entity $entity, string $property) { + $types = $entity->getFieldTypes(); + + if (!isset($types[ $property ])) { + return IQueryBuilder::PARAM_STR; + } + + switch ($types[ $property ]) { + case 'int': + case Types::INTEGER: + case Types::SMALLINT: + return IQueryBuilder::PARAM_INT; + case Types::STRING: + return IQueryBuilder::PARAM_STR; + case 'bool': + case Types::BOOLEAN: + return IQueryBuilder::PARAM_BOOL; + case Types::BLOB: + return IQueryBuilder::PARAM_LOB; + case Types::DATE: + return IQueryBuilder::PARAM_DATETIME_MUTABLE; + case Types::DATETIME: + return IQueryBuilder::PARAM_DATETIME_MUTABLE; + case Types::DATETIME_TZ: + return IQueryBuilder::PARAM_DATETIME_TZ_MUTABLE; + case Types::DATE_IMMUTABLE: + return IQueryBuilder::PARAM_DATE_IMMUTABLE; + case Types::DATETIME_IMMUTABLE: + return IQueryBuilder::PARAM_DATETIME_IMMUTABLE; + case Types::DATETIME_TZ_IMMUTABLE: + return IQueryBuilder::PARAM_DATETIME_TZ_IMMUTABLE; + case Types::TIME: + return IQueryBuilder::PARAM_TIME_MUTABLE; + case Types::TIME_IMMUTABLE: + return IQueryBuilder::PARAM_TIME_IMMUTABLE; + case Types::JSON: + return IQueryBuilder::PARAM_JSON; + } + + return IQueryBuilder::PARAM_STR; + } + + /** + * Returns an db result and throws exceptions when there are more or less + * results + * + * @param IQueryBuilder $query + * @return array the result as row + * @throws Exception + * @throws MultipleObjectsReturnedException if more than one item exist + * @throws DoesNotExistException if the item does not exist + * @see findEntity + * + * @since 14.0.0 + */ + protected function findOneQuery(IQueryBuilder $query): array { + $result = $query->executeQuery(); + + $row = $result->fetch(); + if ($row === false) { + $result->closeCursor(); + $msg = $this->buildDebugMessage( + 'Did expect one result but found none when executing', $query + ); + throw new DoesNotExistException($msg); + } + + $row2 = $result->fetch(); + $result->closeCursor(); + if ($row2 !== false) { + $msg = $this->buildDebugMessage( + 'Did not expect more than one result when executing', $query + ); + throw new MultipleObjectsReturnedException($msg); + } + + return $row; + } + + /** + * @param string $msg + * @param IQueryBuilder $sql + * @return string + * @since 14.0.0 + */ + private function buildDebugMessage(string $msg, IQueryBuilder $sql): string { + return $msg + . ': query "' . $sql->getSQL() . '"; '; + } + + + /** + * Creates an entity from a row. Automatically determines the entity class + * from the current mapper name (MyEntityMapper -> MyEntity) + * + * @param array $row the row which should be converted to an entity + * @return Entity the entity + * @psalm-return T the entity + * @since 14.0.0 + */ + protected function mapRowToEntity(array $row): Entity { + unset($row['DOCTRINE_ROWNUM']); // remove doctrine/dbal helper column + return \call_user_func($this->entityClass . '::fromRow', $row); + } + + + /** + * Runs a sql query and returns an array of entities + * + * @param IQueryBuilder $query + * @return list<Entity> all fetched entities + * @psalm-return list<T> all fetched entities + * @throws Exception + * @since 14.0.0 + */ + protected function findEntities(IQueryBuilder $query): array { + $result = $query->executeQuery(); + try { + $entities = []; + while ($row = $result->fetch()) { + $entities[] = $this->mapRowToEntity($row); + } + return $entities; + } finally { + $result->closeCursor(); + } + } + + /** + * Runs a sql query and yields each resulting entity to obtain database entries in a memory-efficient way + * + * @param IQueryBuilder $query + * @return Generator Generator of fetched entities + * @psalm-return Generator<T> Generator of fetched entities + * @throws Exception + * @since 30.0.0 + */ + protected function yieldEntities(IQueryBuilder $query): Generator { + $result = $query->executeQuery(); + try { + while ($row = $result->fetch()) { + yield $this->mapRowToEntity($row); + } + } finally { + $result->closeCursor(); + } + } + + + /** + * Returns an db result and throws exceptions when there are more or less + * results + * + * @param IQueryBuilder $query + * @return Entity the entity + * @psalm-return T the entity + * @throws Exception + * @throws MultipleObjectsReturnedException if more than one item exist + * @throws DoesNotExistException if the item does not exist + * @since 14.0.0 + */ + protected function findEntity(IQueryBuilder $query): Entity { + return $this->mapRowToEntity($this->findOneQuery($query)); + } +} diff --git a/lib/public/AppFramework/Db/TTransactional.php b/lib/public/AppFramework/Db/TTransactional.php new file mode 100644 index 00000000000..8dd275e5420 --- /dev/null +++ b/lib/public/AppFramework/Db/TTransactional.php @@ -0,0 +1,88 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Db; + +use OC\DB\Exceptions\DbalException; +use OCP\DB\Exception; +use OCP\IDBConnection; +use Throwable; +use function OCP\Log\logger; + +/** + * Helper trait for transactional operations + * + * @since 24.0.0 + */ +trait TTransactional { + /** + * Run an atomic database operation + * + * - Commit if no exceptions are thrown, return the callable result + * - Revert otherwise and rethrows the exception + * + * @template T + * @param callable $fn + * @psalm-param callable():T $fn + * @param IDBConnection $db + * + * @return mixed the result of the passed callable + * @psalm-return T + * + * @throws Exception for possible errors of commit or rollback or the custom operations within the closure + * @throws Throwable any other error caused by the closure + * + * @since 24.0.0 + * @see https://docs.nextcloud.com/server/latest/developer_manual/basics/storage/database.html#transactions + */ + protected function atomic(callable $fn, IDBConnection $db) { + $db->beginTransaction(); + try { + $result = $fn(); + $db->commit(); + return $result; + } catch (Throwable $e) { + $db->rollBack(); + throw $e; + } + } + + /** + * Wrapper around atomic() to retry after a retryable exception occurred + * + * Certain transactions might need to be retried. This is especially useful + * in highly concurrent requests where a deadlocks is thrown by the database + * without waiting for the lock to be freed (e.g. due to MySQL/MariaDB deadlock + * detection) + * + * @template T + * @param callable $fn + * @psalm-param callable():T $fn + * @param IDBConnection $db + * @param int $maxRetries + * + * @return mixed the result of the passed callable + * @psalm-return T + * + * @throws Exception for possible errors of commit or rollback or the custom operations within the closure + * @throws Throwable any other error caused by the closure + * + * @since 27.0.0 + */ + protected function atomicRetry(callable $fn, IDBConnection $db, int $maxRetries = 3): mixed { + for ($i = 0; $i < $maxRetries; $i++) { + try { + return $this->atomic($fn, $db); + } catch (DbalException $e) { + if (!$e->isRetryable() || $i === ($maxRetries - 1)) { + throw $e; + } + logger('core')->warning('Retrying operation after retryable exception.', [ 'exception' => $e ]); + } + } + } +} diff --git a/lib/public/AppFramework/Http.php b/lib/public/AppFramework/Http.php new file mode 100644 index 00000000000..72bdadc9d17 --- /dev/null +++ b/lib/public/AppFramework/Http.php @@ -0,0 +1,309 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCP\AppFramework; + +/** + * Base class which contains constants for HTTP status codes + * @since 6.0.0 + */ +class Http { + /** + * @since 6.0.0 + */ + public const STATUS_CONTINUE = 100; + + /** + * @since 6.0.0 + */ + public const STATUS_SWITCHING_PROTOCOLS = 101; + + /** + * @since 6.0.0 + */ + public const STATUS_PROCESSING = 102; + + /** + * @since 6.0.0 + */ + public const STATUS_OK = 200; + + /** + * @since 6.0.0 + */ + public const STATUS_CREATED = 201; + + /** + * @since 6.0.0 + */ + public const STATUS_ACCEPTED = 202; + + /** + * @since 6.0.0 + */ + public const STATUS_NON_AUTHORATIVE_INFORMATION = 203; + + /** + * @since 6.0.0 + */ + public const STATUS_NO_CONTENT = 204; + + /** + * @since 6.0.0 + */ + public const STATUS_RESET_CONTENT = 205; + + /** + * @since 6.0.0 + */ + public const STATUS_PARTIAL_CONTENT = 206; + + /** + * @since 6.0.0 + */ + public const STATUS_MULTI_STATUS = 207; + + /** + * @since 6.0.0 + */ + public const STATUS_ALREADY_REPORTED = 208; + + /** + * @since 6.0.0 + */ + public const STATUS_IM_USED = 226; + + /** + * @since 6.0.0 + */ + public const STATUS_MULTIPLE_CHOICES = 300; + + /** + * @since 6.0.0 + */ + public const STATUS_MOVED_PERMANENTLY = 301; + + /** + * @since 6.0.0 + */ + public const STATUS_FOUND = 302; + + /** + * @since 6.0.0 + */ + public const STATUS_SEE_OTHER = 303; + + /** + * @since 6.0.0 + */ + public const STATUS_NOT_MODIFIED = 304; + + /** + * @since 6.0.0 + */ + public const STATUS_USE_PROXY = 305; + + /** + * @since 6.0.0 + */ + public const STATUS_RESERVED = 306; + + /** + * @since 6.0.0 + */ + public const STATUS_TEMPORARY_REDIRECT = 307; + + /** + * @since 6.0.0 + */ + public const STATUS_BAD_REQUEST = 400; + + /** + * @since 6.0.0 + */ + public const STATUS_UNAUTHORIZED = 401; + + /** + * @since 6.0.0 + */ + public const STATUS_PAYMENT_REQUIRED = 402; + + /** + * @since 6.0.0 + */ + public const STATUS_FORBIDDEN = 403; + + /** + * @since 6.0.0 + */ + public const STATUS_NOT_FOUND = 404; + + /** + * @since 6.0.0 + */ + public const STATUS_METHOD_NOT_ALLOWED = 405; + + /** + * @since 6.0.0 + */ + public const STATUS_NOT_ACCEPTABLE = 406; + + /** + * @since 6.0.0 + */ + public const STATUS_PROXY_AUTHENTICATION_REQUIRED = 407; + + /** + * @since 6.0.0 + */ + public const STATUS_REQUEST_TIMEOUT = 408; + + /** + * @since 6.0.0 + */ + public const STATUS_CONFLICT = 409; + + /** + * @since 6.0.0 + */ + public const STATUS_GONE = 410; + + /** + * @since 6.0.0 + */ + public const STATUS_LENGTH_REQUIRED = 411; + + /** + * @since 6.0.0 + */ + public const STATUS_PRECONDITION_FAILED = 412; + + /** + * @since 6.0.0 + */ + public const STATUS_REQUEST_ENTITY_TOO_LARGE = 413; + + /** + * @since 6.0.0 + */ + public const STATUS_REQUEST_URI_TOO_LONG = 414; + + /** + * @since 6.0.0 + */ + public const STATUS_UNSUPPORTED_MEDIA_TYPE = 415; + + /** + * @since 6.0.0 + */ + public const STATUS_REQUEST_RANGE_NOT_SATISFIABLE = 416; + + /** + * @since 6.0.0 + */ + public const STATUS_EXPECTATION_FAILED = 417; + + /** + * @since 6.0.0 + */ + public const STATUS_IM_A_TEAPOT = 418; + + /** + * @since 6.0.0 + */ + public const STATUS_UNPROCESSABLE_ENTITY = 422; + + /** + * @since 6.0.0 + */ + public const STATUS_LOCKED = 423; + + /** + * @since 6.0.0 + */ + public const STATUS_FAILED_DEPENDENCY = 424; + + /** + * @since 6.0.0 + */ + public const STATUS_UPGRADE_REQUIRED = 426; + + /** + * @since 6.0.0 + */ + public const STATUS_PRECONDITION_REQUIRED = 428; + + /** + * @since 6.0.0 + */ + public const STATUS_TOO_MANY_REQUESTS = 429; + + /** + * @since 6.0.0 + */ + public const STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE = 431; + + /** + * @since 6.0.0 + */ + public const STATUS_INTERNAL_SERVER_ERROR = 500; + + /** + * @since 6.0.0 + */ + public const STATUS_NOT_IMPLEMENTED = 501; + + /** + * @since 6.0.0 + */ + public const STATUS_BAD_GATEWAY = 502; + + /** + * @since 6.0.0 + */ + public const STATUS_SERVICE_UNAVAILABLE = 503; + + /** + * @since 6.0.0 + */ + public const STATUS_GATEWAY_TIMEOUT = 504; + + /** + * @since 6.0.0 + */ + public const STATUS_HTTP_VERSION_NOT_SUPPORTED = 505; + + /** + * @since 6.0.0 + */ + public const STATUS_VARIANT_ALSO_NEGOTIATES = 506; + + /** + * @since 6.0.0 + */ + public const STATUS_INSUFFICIENT_STORAGE = 507; + + /** + * @since 6.0.0 + */ + public const STATUS_LOOP_DETECTED = 508; + + /** + * @since 6.0.0 + */ + public const STATUS_BANDWIDTH_LIMIT_EXCEEDED = 509; + + /** + * @since 6.0.0 + */ + public const STATUS_NOT_EXTENDED = 510; + + /** + * @since 6.0.0 + */ + public const STATUS_NETWORK_AUTHENTICATION_REQUIRED = 511; +} diff --git a/lib/public/AppFramework/Http/Attribute/ARateLimit.php b/lib/public/AppFramework/Http/Attribute/ARateLimit.php new file mode 100644 index 00000000000..c06b1180ae3 --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/ARateLimit.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Http\Attribute; + +/** + * Attribute for controller methods that want to limit the times a logged-in + * user can call the endpoint in a given time period. + * + * @since 27.0.0 + */ +abstract class ARateLimit { + /** + * @param int $limit The maximum number of requests that can be made in the given period in seconds. + * @param int $period The time period in seconds. + * @since 27.0.0 + */ + public function __construct( + protected int $limit, + protected int $period, + ) { + } + + /** + * @since 27.0.0 + */ + public function getLimit(): int { + return $this->limit; + } + + /** + * @since 27.0.0 + */ + public function getPeriod(): int { + return $this->period; + } +} diff --git a/lib/public/AppFramework/Http/Attribute/AnonRateLimit.php b/lib/public/AppFramework/Http/Attribute/AnonRateLimit.php new file mode 100644 index 00000000000..f02f2b695c5 --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/AnonRateLimit.php @@ -0,0 +1,22 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; + +/** + * Attribute for controller methods that want to limit the times a not logged-in + * guest can call the endpoint in a given time period. + * + * @since 27.0.0 + */ +#[Attribute(Attribute::TARGET_METHOD)] +class AnonRateLimit extends ARateLimit { +} diff --git a/lib/public/AppFramework/Http/Attribute/ApiRoute.php b/lib/public/AppFramework/Http/Attribute/ApiRoute.php new file mode 100644 index 00000000000..1d61cfe7704 --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/ApiRoute.php @@ -0,0 +1,47 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; + +/** + * This attribute can be used to define API routes on controller methods. + * + * It works in addition to the traditional routes.php method and has the same parameters + * (except for the `name` parameter which is not needed). + * + * @since 29.0.0 + */ +#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +class ApiRoute extends Route { + /** + * @inheritDoc + * + * @since 29.0.0 + */ + public function __construct( + protected string $verb, + protected string $url, + protected ?array $requirements = null, + protected ?array $defaults = null, + protected ?string $root = null, + protected ?string $postfix = null, + ) { + parent::__construct( + Route::TYPE_API, + $verb, + $url, + $requirements, + $defaults, + $root, + $postfix, + ); + } +} diff --git a/lib/public/AppFramework/Http/Attribute/AppApiAdminAccessWithoutUser.php b/lib/public/AppFramework/Http/Attribute/AppApiAdminAccessWithoutUser.php new file mode 100644 index 00000000000..6b78fee41af --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/AppApiAdminAccessWithoutUser.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; + +/** + * Attribute for (sub)administrator controller methods that allow access for ExApps when the User is not set. + * + * @since 30.0.0 + */ +#[Attribute] +class AppApiAdminAccessWithoutUser { +} diff --git a/lib/public/AppFramework/Http/Attribute/AuthorizedAdminSetting.php b/lib/public/AppFramework/Http/Attribute/AuthorizedAdminSetting.php new file mode 100644 index 00000000000..83101143fc9 --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/AuthorizedAdminSetting.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; +use OCP\Settings\IDelegatedSettings; + +/** + * Attribute for controller methods that should be only accessible with + * full admin or partial admin permissions. + * + * @since 27.0.0 + */ +#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +class AuthorizedAdminSetting { + /** + * @param class-string<IDelegatedSettings> $settings A settings section the user needs to be able to access + * @since 27.0.0 + */ + public function __construct( + protected string $settings, + ) { + } + + /** + * + * @return class-string<IDelegatedSettings> + * @since 27.0.0 + */ + public function getSettings(): string { + return $this->settings; + } +} diff --git a/lib/public/AppFramework/Http/Attribute/BruteForceProtection.php b/lib/public/AppFramework/Http/Attribute/BruteForceProtection.php new file mode 100644 index 00000000000..0fc1a3b9b6d --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/BruteForceProtection.php @@ -0,0 +1,36 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; + +/** + * Attribute for controller methods that want to protect passwords, keys, tokens + * or other data against brute force + * + * @since 27.0.0 + */ +#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +class BruteForceProtection { + /** + * @since 27.0.0 + */ + public function __construct( + protected string $action, + ) { + } + + /** + * @since 27.0.0 + */ + public function getAction(): string { + return $this->action; + } +} diff --git a/lib/public/AppFramework/Http/Attribute/CORS.php b/lib/public/AppFramework/Http/Attribute/CORS.php new file mode 100644 index 00000000000..ff639635635 --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/CORS.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; + +/** + * Attribute for controller methods that can also be accessed by other websites. + * See https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS for an explanation of the functionality and the security implications. + * See https://docs.nextcloud.com/server/latest/developer_manual/digging_deeper/rest_apis.html on how to implement it in your controller. + * + * @since 27.0.0 + */ +#[Attribute] +class CORS { +} diff --git a/lib/public/AppFramework/Http/Attribute/ExAppRequired.php b/lib/public/AppFramework/Http/Attribute/ExAppRequired.php new file mode 100644 index 00000000000..eb18da8027c --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/ExAppRequired.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; + +/** + * Attribute for controller methods that can only be accessed by ExApps + * + * @since 30.0.0 + */ +#[Attribute] +class ExAppRequired { +} diff --git a/lib/public/AppFramework/Http/Attribute/FrontpageRoute.php b/lib/public/AppFramework/Http/Attribute/FrontpageRoute.php new file mode 100644 index 00000000000..398116d786f --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/FrontpageRoute.php @@ -0,0 +1,47 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; + +/** + * This attribute can be used to define Frontpage routes on controller methods. + * + * It works in addition to the traditional routes.php method and has the same parameters + * (except for the `name` parameter which is not needed). + * + * @since 29.0.0 + */ +#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +class FrontpageRoute extends Route { + /** + * @inheritDoc + * + * @since 29.0.0 + */ + public function __construct( + protected string $verb, + protected string $url, + protected ?array $requirements = null, + protected ?array $defaults = null, + protected ?string $root = null, + protected ?string $postfix = null, + ) { + parent::__construct( + Route::TYPE_FRONTPAGE, + $verb, + $url, + $requirements, + $defaults, + $root, + $postfix, + ); + } +} diff --git a/lib/public/AppFramework/Http/Attribute/IgnoreOpenAPI.php b/lib/public/AppFramework/Http/Attribute/IgnoreOpenAPI.php new file mode 100644 index 00000000000..114637935db --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/IgnoreOpenAPI.php @@ -0,0 +1,22 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; + +/** + * Attribute for controller methods that should be ignored when generating OpenAPI documentation + * + * @since 28.0.0 + * @deprecated 28.0.0 Use {@see OpenAPI} with {@see OpenAPI::SCOPE_IGNORE} instead: `#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]` + */ +#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)] +class IgnoreOpenAPI { +} diff --git a/lib/public/AppFramework/Http/Attribute/NoAdminRequired.php b/lib/public/AppFramework/Http/Attribute/NoAdminRequired.php new file mode 100644 index 00000000000..59c6cf86800 --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/NoAdminRequired.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; + +/** + * Attribute for controller methods that can be accessed by any logged-in user + * + * @since 27.0.0 + */ +#[Attribute] +class NoAdminRequired { +} diff --git a/lib/public/AppFramework/Http/Attribute/NoCSRFRequired.php b/lib/public/AppFramework/Http/Attribute/NoCSRFRequired.php new file mode 100644 index 00000000000..ad7e569a3b9 --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/NoCSRFRequired.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; + +/** + * Attribute for controller methods that are not CSRF protected + * + * @since 27.0.0 + */ +#[Attribute] +class NoCSRFRequired { +} diff --git a/lib/public/AppFramework/Http/Attribute/OpenAPI.php b/lib/public/AppFramework/Http/Attribute/OpenAPI.php new file mode 100644 index 00000000000..1b44b2a57fe --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/OpenAPI.php @@ -0,0 +1,91 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; + +/** + * With this attribute a controller or a method can be moved into a different + * scope or tag. Scopes should be seen as API consumers, tags can be used to group + * different routes inside the same scope. + * + * @since 28.0.0 + */ +#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] +class OpenAPI { + /** + * APIs used for normal user facing interaction with your app, + * e.g. when you would implement a mobile client or standalone frontend. + * + * @since 28.0.0 + */ + public const SCOPE_DEFAULT = 'default'; + + /** + * APIs used to administrate your app's configuration on an administrative level. + * Will be set automatically when admin permissions are required to access the route. + * + * @since 28.0.0 + */ + public const SCOPE_ADMINISTRATION = 'administration'; + + /** + * APIs used by servers to federate with each other. + * + * @since 28.0.0 + */ + public const SCOPE_FEDERATION = 'federation'; + + /** + * Ignore this controller or method in all generated OpenAPI specifications. + * + * @since 28.0.0 + */ + public const SCOPE_IGNORE = 'ignore'; + + /** + * APIs used by ExApps. + * Will be set automatically when an ExApp is required to access the route. + * + * @since 30.0.0 + */ + public const SCOPE_EX_APP = 'ex_app'; + + /** + * @param self::SCOPE_*|string $scope Scopes are used to define different clients. + * It is recommended to go with the scopes available as self::SCOPE_* constants, + * but in exotic cases other APIs might need documentation as well, + * then a free string can be provided (but it should be `a-z` only). + * @param ?list<string> $tags Tags can be used to group routes inside a scope + * for easier implementation and reviewing of the API specification. + * It defaults to the controller name in snake_case (should be `a-z` and underscore only). + * @since 28.0.0 + */ + public function __construct( + protected string $scope = self::SCOPE_DEFAULT, + protected ?array $tags = null, + ) { + } + + /** + * @since 28.0.0 + */ + public function getScope(): string { + return $this->scope; + } + + /** + * @return ?list<string> + * @since 28.0.0 + */ + public function getTags(): ?array { + return $this->tags; + } +} diff --git a/lib/public/AppFramework/Http/Attribute/PasswordConfirmationRequired.php b/lib/public/AppFramework/Http/Attribute/PasswordConfirmationRequired.php new file mode 100644 index 00000000000..c41e5aa2445 --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/PasswordConfirmationRequired.php @@ -0,0 +1,38 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; + +/** + * Attribute for controller methods that require the password to be confirmed with in the last 30 minutes + * + * @since 27.0.0 + */ +#[Attribute] +class PasswordConfirmationRequired { + /** + * @param bool $strict - Whether password confirmation needs to happen in the request. + * + * @since 31.0.0 + */ + public function __construct( + protected bool $strict = false, + ) { + } + + /** + * @since 31.0.0 + */ + public function getStrict(): bool { + return $this->strict; + } + +} diff --git a/lib/public/AppFramework/Http/Attribute/PublicPage.php b/lib/public/AppFramework/Http/Attribute/PublicPage.php new file mode 100644 index 00000000000..85c1ed06f80 --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/PublicPage.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; + +/** + * Attribute for controller methods that can also be accessed by not logged-in user + * + * @since 27.0.0 + */ +#[Attribute] +class PublicPage { +} diff --git a/lib/public/AppFramework/Http/Attribute/RequestHeader.php b/lib/public/AppFramework/Http/Attribute/RequestHeader.php new file mode 100644 index 00000000000..1d0fbbfa0c3 --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/RequestHeader.php @@ -0,0 +1,34 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; + +/** + * This attribute allows documenting request headers and is primarily intended for OpenAPI documentation. + * It should be added whenever you use a request header in a controller method, in order to properly describe the header and its functionality. + * There are no checks that ensure the header is set, so you will still need to do this yourself in the controller method. + * + * @since 32.0.0 + */ +#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +class RequestHeader { + /** + * @param lowercase-string $name The name of the request header + * @param non-empty-string $description The description of the request header + * @param bool $indirect Allow indirect usage of the header for example in a middleware. Enabling this turns off the check which ensures that the header must be referenced in the controller method. + */ + public function __construct( + protected string $name, + protected string $description, + protected bool $indirect = false, + ) { + } +} diff --git a/lib/public/AppFramework/Http/Attribute/Route.php b/lib/public/AppFramework/Http/Attribute/Route.php new file mode 100644 index 00000000000..45e977d64f8 --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/Route.php @@ -0,0 +1,145 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; + +/** + * This attribute can be used to define routes on controller methods. + * + * It works in addition to the traditional routes.php method and has the same parameters + * (except for the `name` parameter which is not needed). + * + * @since 29.0.0 + */ +#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +class Route { + + /** + * Corresponds to the `ocs` key in routes.php + * + * @see ApiRoute + * @since 29.0.0 + */ + public const TYPE_API = 'ocs'; + + /** + * Corresponds to the `routes` key in routes.php + * + * @see FrontpageRoute + * @since 29.0.0 + */ + public const TYPE_FRONTPAGE = 'routes'; + + /** + * @param string $type Either Route::TYPE_API or Route::TYPE_FRONTPAGE. + * @psalm-param Route::TYPE_* $type + * @param string $verb HTTP method of the route. + * @psalm-param 'GET'|'HEAD'|'POST'|'PUT'|'DELETE'|'OPTIONS'|'PATCH' $verb + * @param string $url The path of the route. + * @param ?array<string, string> $requirements Array of regexes mapped to the path parameters. + * @param ?array<string, mixed> $defaults Array of default values mapped to the path parameters. + * @param ?string $root Custom root. For OCS all apps are allowed, but for index.php only some can use it. + * @param ?string $postfix Postfix for the route name. + * @since 29.0.0 + */ + public function __construct( + protected string $type, + protected string $verb, + protected string $url, + protected ?array $requirements = null, + protected ?array $defaults = null, + protected ?string $root = null, + protected ?string $postfix = null, + ) { + } + + /** + * @return array{ + * verb: string, + * url: string, + * requirements?: array<string, string>, + * defaults?: array<string, mixed>, + * root?: string, + * postfix?: string, + * } + * @since 29.0.0 + */ + public function toArray() { + $route = [ + 'verb' => $this->verb, + 'url' => $this->url, + ]; + + if ($this->requirements !== null) { + $route['requirements'] = $this->requirements; + } + if ($this->defaults !== null) { + $route['defaults'] = $this->defaults; + } + if ($this->root !== null) { + $route['root'] = $this->root; + } + if ($this->postfix !== null) { + $route['postfix'] = $this->postfix; + } + + return $route; + } + + /** + * @since 29.0.0 + */ + public function getType(): string { + return $this->type; + } + + /** + * @since 29.0.0 + */ + public function getVerb(): string { + return $this->verb; + } + + /** + * @since 29.0.0 + */ + public function getUrl(): string { + return $this->url; + } + + /** + * @since 29.0.0 + */ + public function getRequirements(): ?array { + return $this->requirements; + } + + /** + * @since 29.0.0 + */ + public function getDefaults(): ?array { + return $this->defaults; + } + + /** + * @since 29.0.0 + */ + public function getRoot(): ?string { + return $this->root; + } + + /** + * @since 29.0.0 + */ + public function getPostfix(): ?string { + return $this->postfix; + } +} diff --git a/lib/public/AppFramework/Http/Attribute/StrictCookiesRequired.php b/lib/public/AppFramework/Http/Attribute/StrictCookiesRequired.php new file mode 100644 index 00000000000..a2697847ca6 --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/StrictCookiesRequired.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; + +/** + * Attribute for controller methods that require strict cookies + * + * @since 27.0.0 + */ +#[Attribute] +class StrictCookiesRequired { +} diff --git a/lib/public/AppFramework/Http/Attribute/SubAdminRequired.php b/lib/public/AppFramework/Http/Attribute/SubAdminRequired.php new file mode 100644 index 00000000000..38c4dd35f3c --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/SubAdminRequired.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; + +/** + * Attribute for controller methods that can be accessed by sub-admins + * + * @since 27.0.0 + */ +#[Attribute] +class SubAdminRequired { +} diff --git a/lib/public/AppFramework/Http/Attribute/UseSession.php b/lib/public/AppFramework/Http/Attribute/UseSession.php new file mode 100644 index 00000000000..f64b050144f --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/UseSession.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; + +/** + * Attribute for controller methods that need to read/write PHP session data + * + * @since 26.0.0 + */ +#[Attribute] +class UseSession { +} diff --git a/lib/public/AppFramework/Http/Attribute/UserRateLimit.php b/lib/public/AppFramework/Http/Attribute/UserRateLimit.php new file mode 100644 index 00000000000..6fcf7127e89 --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/UserRateLimit.php @@ -0,0 +1,22 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; + +/** + * Attribute for controller methods that want to limit the times a logged-in + * user can call the endpoint in a given time period. + * + * @since 27.0.0 + */ +#[Attribute(Attribute::TARGET_METHOD)] +class UserRateLimit extends ARateLimit { +} diff --git a/lib/public/AppFramework/Http/ContentSecurityPolicy.php b/lib/public/AppFramework/Http/ContentSecurityPolicy.php new file mode 100644 index 00000000000..11ec79bbdb7 --- /dev/null +++ b/lib/public/AppFramework/Http/ContentSecurityPolicy.php @@ -0,0 +1,90 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCP\AppFramework\Http; + +/** + * Class ContentSecurityPolicy is a simple helper which allows applications to + * modify the Content-Security-Policy sent by Nextcloud. Per default only JavaScript, + * stylesheets, images, fonts, media and connections from the same domain + * ('self') are allowed. + * + * Even if a value gets modified above defaults will still get appended. Please + * notice that Nextcloud ships already with sensible defaults and those policies + * should require no modification at all for most use-cases. + * + * This class allows unsafe-inline of CSS. + * + * @since 8.1.0 + */ +class ContentSecurityPolicy extends EmptyContentSecurityPolicy { + /** @var bool Whether inline JS snippets are allowed */ + protected $inlineScriptAllowed = false; + /** @var bool Whether eval in JS scripts is allowed */ + protected $evalScriptAllowed = false; + /** @var bool Whether WebAssembly compilation is allowed */ + protected ?bool $evalWasmAllowed = false; + /** @var bool Whether strict-dynamic should be set */ + protected $strictDynamicAllowed = false; + /** @var bool Whether strict-dynamic should be set for 'script-src-elem' */ + protected $strictDynamicAllowedOnScripts = true; + /** @var array Domains from which scripts can get loaded */ + protected $allowedScriptDomains = [ + '\'self\'', + ]; + /** + * @var bool Whether inline CSS is allowed + * TODO: Disallow per default + * @link https://github.com/owncloud/core/issues/13458 + */ + protected $inlineStyleAllowed = true; + /** @var array Domains from which CSS can get loaded */ + protected $allowedStyleDomains = [ + '\'self\'', + ]; + /** @var array Domains from which images can get loaded */ + protected $allowedImageDomains = [ + '\'self\'', + 'data:', + 'blob:', + ]; + /** @var array Domains to which connections can be done */ + protected $allowedConnectDomains = [ + '\'self\'', + ]; + /** @var array Domains from which media elements can be loaded */ + protected $allowedMediaDomains = [ + '\'self\'', + ]; + /** @var array Domains from which object elements can be loaded */ + protected $allowedObjectDomains = []; + /** @var array Domains from which iframes can be loaded */ + protected $allowedFrameDomains = []; + /** @var array Domains from which fonts can be loaded */ + protected $allowedFontDomains = [ + '\'self\'', + 'data:', + ]; + /** @var array Domains from which web-workers and nested browsing content can load elements */ + protected $allowedChildSrcDomains = []; + + /** @var array Domains which can embed this Nextcloud instance */ + protected $allowedFrameAncestors = [ + '\'self\'', + ]; + + /** @var array Domains from which web-workers can be loaded */ + protected $allowedWorkerSrcDomains = []; + + /** @var array Domains which can be used as target for forms */ + protected $allowedFormActionDomains = [ + '\'self\'', + ]; + + /** @var array Locations to report violations to */ + protected $reportTo = []; +} diff --git a/lib/public/AppFramework/Http/DataDisplayResponse.php b/lib/public/AppFramework/Http/DataDisplayResponse.php new file mode 100644 index 00000000000..e1ded910328 --- /dev/null +++ b/lib/public/AppFramework/Http/DataDisplayResponse.php @@ -0,0 +1,72 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCP\AppFramework\Http; + +use OCP\AppFramework\Http; + +/** + * Class DataDisplayResponse + * + * @since 8.1.0 + * @template S of Http::STATUS_* + * @template H of array<string, mixed> + * @template-extends Response<Http::STATUS_*, array<string, mixed>> + */ +class DataDisplayResponse extends Response { + /** + * response data + * @var string + */ + protected $data; + + + /** + * @param string $data the data to display + * @param S $statusCode the Http status code, defaults to 200 + * @param H $headers additional key value based headers + * @since 8.1.0 + */ + public function __construct(string $data = '', int $statusCode = Http::STATUS_OK, array $headers = []) { + parent::__construct($statusCode, $headers); + + $this->data = $data; + $this->addHeader('Content-Disposition', 'inline; filename=""'); + } + + /** + * Outputs data. No processing is done. + * @return string + * @since 8.1.0 + */ + public function render() { + return $this->data; + } + + + /** + * Sets values in the data + * @param string $data the data to display + * @return DataDisplayResponse Reference to this object + * @since 8.1.0 + */ + public function setData($data) { + $this->data = $data; + + return $this; + } + + + /** + * Used to get the set parameters + * @return string the data + * @since 8.1.0 + */ + public function getData() { + return $this->data; + } +} diff --git a/lib/public/AppFramework/Http/DataDownloadResponse.php b/lib/public/AppFramework/Http/DataDownloadResponse.php new file mode 100644 index 00000000000..ee6bcf0d0c5 --- /dev/null +++ b/lib/public/AppFramework/Http/DataDownloadResponse.php @@ -0,0 +1,56 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCP\AppFramework\Http; + +use OCP\AppFramework\Http; + +/** + * Class DataDownloadResponse + * + * @since 8.0.0 + * @template S of Http::STATUS_* + * @template C of string + * @template H of array<string, mixed> + * @template-extends DownloadResponse<Http::STATUS_*, string, array<string, mixed>> + */ +class DataDownloadResponse extends DownloadResponse { + /** + * @var string + */ + private $data; + + /** + * Creates a response that prompts the user to download the text + * @param string $data text to be downloaded + * @param string $filename the name that the downloaded file should have + * @param C $contentType the mimetype that the downloaded file should have + * @param S $status + * @param H $headers + * @since 8.0.0 + */ + public function __construct(string $data, string $filename, string $contentType, int $status = Http::STATUS_OK, array $headers = []) { + $this->data = $data; + parent::__construct($filename, $contentType, $status, $headers); + } + + /** + * @param string $data + * @since 8.0.0 + */ + public function setData($data) { + $this->data = $data; + } + + /** + * @return string + * @since 8.0.0 + */ + public function render() { + return $this->data; + } +} diff --git a/lib/public/AppFramework/Http/DataResponse.php b/lib/public/AppFramework/Http/DataResponse.php new file mode 100644 index 00000000000..2b54ce848ef --- /dev/null +++ b/lib/public/AppFramework/Http/DataResponse.php @@ -0,0 +1,65 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCP\AppFramework\Http; + +use OCP\AppFramework\Http; + +/** + * A generic DataResponse class that is used to return generic data responses + * for responders to transform + * @since 8.0.0 + * @psalm-type DataResponseType = array|int|float|string|bool|object|null|\stdClass|\JsonSerializable + * @template S of Http::STATUS_* + * @template-covariant T of DataResponseType + * @template H of array<string, mixed> + * @template-extends Response<Http::STATUS_*, array<string, mixed>> + */ +class DataResponse extends Response { + /** + * response data + * @var T + */ + protected $data; + + + /** + * @param T $data the object or array that should be transformed + * @param S $statusCode the Http status code, defaults to 200 + * @param H $headers additional key value based headers + * @since 8.0.0 + */ + public function __construct(mixed $data = [], int $statusCode = Http::STATUS_OK, array $headers = []) { + parent::__construct($statusCode, $headers); + + $this->data = $data; + } + + + /** + * Sets values in the data json array + * @psalm-suppress InvalidTemplateParam + * @param T $data an array or object which will be transformed + * @return DataResponse Reference to this object + * @since 8.0.0 + */ + public function setData($data) { + $this->data = $data; + + return $this; + } + + + /** + * Used to get the set parameters + * @return T the data + * @since 8.0.0 + */ + public function getData() { + return $this->data; + } +} diff --git a/lib/public/AppFramework/Http/DownloadResponse.php b/lib/public/AppFramework/Http/DownloadResponse.php new file mode 100644 index 00000000000..190de022d36 --- /dev/null +++ b/lib/public/AppFramework/Http/DownloadResponse.php @@ -0,0 +1,37 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCP\AppFramework\Http; + +use OCP\AppFramework\Http; + +/** + * Prompts the user to download the a file + * @since 7.0.0 + * @template S of Http::STATUS_* + * @template C of string + * @template H of array<string, mixed> + * @template-extends Response<Http::STATUS_*, array<string, mixed>> + */ +class DownloadResponse extends Response { + /** + * Creates a response that prompts the user to download the file + * @param string $filename the name that the downloaded file should have + * @param C $contentType the mimetype that the downloaded file should have + * @param S $status + * @param H $headers + * @since 7.0.0 + */ + public function __construct(string $filename, string $contentType, int $status = Http::STATUS_OK, array $headers = []) { + parent::__construct($status, $headers); + + $filename = strtr($filename, ['"' => '\\"', '\\' => '\\\\']); + + $this->addHeader('Content-Disposition', 'attachment; filename="' . $filename . '"'); + $this->addHeader('Content-Type', $contentType); + } +} diff --git a/lib/public/AppFramework/Http/EmptyContentSecurityPolicy.php b/lib/public/AppFramework/Http/EmptyContentSecurityPolicy.php new file mode 100644 index 00000000000..b8bbfdb7d67 --- /dev/null +++ b/lib/public/AppFramework/Http/EmptyContentSecurityPolicy.php @@ -0,0 +1,549 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCP\AppFramework\Http; + +/** + * Class EmptyContentSecurityPolicy is a simple helper which allows applications + * to modify the Content-Security-Policy sent by Nexcloud. Per default the policy + * is forbidding everything. + * + * As alternative with sane exemptions look at ContentSecurityPolicy + * + * @see \OCP\AppFramework\Http\ContentSecurityPolicy + * @since 9.0.0 + */ +class EmptyContentSecurityPolicy { + /** @var ?string JS nonce to be used */ + protected ?string $jsNonce = null; + /** @var bool Whether strict-dynamic should be used */ + protected $strictDynamicAllowed = null; + /** @var bool Whether strict-dynamic should be used on script-src-elem */ + protected $strictDynamicAllowedOnScripts = null; + /** + * @var bool Whether eval in JS scripts is allowed + * TODO: Disallow per default + * @link https://github.com/owncloud/core/issues/11925 + */ + protected $evalScriptAllowed = null; + /** @var bool Whether WebAssembly compilation is allowed */ + protected ?bool $evalWasmAllowed = null; + /** @var array Domains from which scripts can get loaded */ + protected $allowedScriptDomains = null; + /** + * @var bool Whether inline CSS is allowed + * TODO: Disallow per default + * @link https://github.com/owncloud/core/issues/13458 + */ + protected $inlineStyleAllowed = null; + /** @var array Domains from which CSS can get loaded */ + protected $allowedStyleDomains = null; + /** @var array Domains from which images can get loaded */ + protected $allowedImageDomains = null; + /** @var array Domains to which connections can be done */ + protected $allowedConnectDomains = null; + /** @var array Domains from which media elements can be loaded */ + protected $allowedMediaDomains = null; + /** @var array Domains from which object elements can be loaded */ + protected $allowedObjectDomains = null; + /** @var array Domains from which iframes can be loaded */ + protected $allowedFrameDomains = null; + /** @var array Domains from which fonts can be loaded */ + protected $allowedFontDomains = null; + /** @var array Domains from which web-workers and nested browsing content can load elements */ + protected $allowedChildSrcDomains = null; + /** @var array Domains which can embed this Nextcloud instance */ + protected $allowedFrameAncestors = null; + /** @var array Domains from which web-workers can be loaded */ + protected $allowedWorkerSrcDomains = null; + /** @var array Domains which can be used as target for forms */ + protected $allowedFormActionDomains = null; + + /** @var array Locations to report violations to */ + protected $reportTo = null; + + /** + * @param bool $state + * @return EmptyContentSecurityPolicy + * @since 24.0.0 + */ + public function useStrictDynamic(bool $state = false): self { + $this->strictDynamicAllowed = $state; + return $this; + } + + /** + * In contrast to `useStrictDynamic` this only sets strict-dynamic on script-src-elem + * Meaning only grants trust to all imports of scripts that were loaded in `<script>` tags, and thus weakens less the CSP. + * @param bool $state + * @return EmptyContentSecurityPolicy + * @since 28.0.0 + */ + public function useStrictDynamicOnScripts(bool $state = false): self { + $this->strictDynamicAllowedOnScripts = $state; + return $this; + } + + /** + * The base64 encoded nonce to be used for script source. + * This method is only for CSPMiddleware, custom values are ignored in mergePolicies of ContentSecurityPolicyManager + * + * @param string $nonce + * @return $this + * @since 11.0.0 + */ + public function useJsNonce($nonce) { + $this->jsNonce = $nonce; + return $this; + } + + /** + * Whether eval in JavaScript is allowed or forbidden + * @param bool $state + * @return $this + * @since 8.1.0 + * @deprecated 17.0.0 Eval should not be used anymore. Please update your scripts. This function will stop functioning in a future version of Nextcloud. + */ + public function allowEvalScript($state = true) { + $this->evalScriptAllowed = $state; + return $this; + } + + /** + * Whether WebAssembly compilation is allowed or forbidden + * @param bool $state + * @return $this + * @since 28.0.0 + */ + public function allowEvalWasm(bool $state = true) { + $this->evalWasmAllowed = $state; + return $this; + } + + /** + * Allows to execute JavaScript files from a specific domain. Use * to + * allow JavaScript from all domains. + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 8.1.0 + */ + public function addAllowedScriptDomain($domain) { + $this->allowedScriptDomains[] = $domain; + return $this; + } + + /** + * Remove the specified allowed script domain from the allowed domains. + * + * @param string $domain + * @return $this + * @since 8.1.0 + */ + public function disallowScriptDomain($domain) { + $this->allowedScriptDomains = array_diff($this->allowedScriptDomains, [$domain]); + return $this; + } + + /** + * Whether inline CSS snippets are allowed or forbidden + * @param bool $state + * @return $this + * @since 8.1.0 + */ + public function allowInlineStyle($state = true) { + $this->inlineStyleAllowed = $state; + return $this; + } + + /** + * Allows to execute CSS files from a specific domain. Use * to allow + * CSS from all domains. + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 8.1.0 + */ + public function addAllowedStyleDomain($domain) { + $this->allowedStyleDomains[] = $domain; + return $this; + } + + /** + * Remove the specified allowed style domain from the allowed domains. + * + * @param string $domain + * @return $this + * @since 8.1.0 + */ + public function disallowStyleDomain($domain) { + $this->allowedStyleDomains = array_diff($this->allowedStyleDomains, [$domain]); + return $this; + } + + /** + * Allows using fonts from a specific domain. Use * to allow + * fonts from all domains. + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 8.1.0 + */ + public function addAllowedFontDomain($domain) { + $this->allowedFontDomains[] = $domain; + return $this; + } + + /** + * Remove the specified allowed font domain from the allowed domains. + * + * @param string $domain + * @return $this + * @since 8.1.0 + */ + public function disallowFontDomain($domain) { + $this->allowedFontDomains = array_diff($this->allowedFontDomains, [$domain]); + return $this; + } + + /** + * Allows embedding images from a specific domain. Use * to allow + * images from all domains. + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 8.1.0 + */ + public function addAllowedImageDomain($domain) { + $this->allowedImageDomains[] = $domain; + return $this; + } + + /** + * Remove the specified allowed image domain from the allowed domains. + * + * @param string $domain + * @return $this + * @since 8.1.0 + */ + public function disallowImageDomain($domain) { + $this->allowedImageDomains = array_diff($this->allowedImageDomains, [$domain]); + return $this; + } + + /** + * To which remote domains the JS connect to. + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 8.1.0 + */ + public function addAllowedConnectDomain($domain) { + $this->allowedConnectDomains[] = $domain; + return $this; + } + + /** + * Remove the specified allowed connect domain from the allowed domains. + * + * @param string $domain + * @return $this + * @since 8.1.0 + */ + public function disallowConnectDomain($domain) { + $this->allowedConnectDomains = array_diff($this->allowedConnectDomains, [$domain]); + return $this; + } + + /** + * From which domains media elements can be embedded. + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 8.1.0 + */ + public function addAllowedMediaDomain($domain) { + $this->allowedMediaDomains[] = $domain; + return $this; + } + + /** + * Remove the specified allowed media domain from the allowed domains. + * + * @param string $domain + * @return $this + * @since 8.1.0 + */ + public function disallowMediaDomain($domain) { + $this->allowedMediaDomains = array_diff($this->allowedMediaDomains, [$domain]); + return $this; + } + + /** + * From which domains objects such as <object>, <embed> or <applet> are executed + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 8.1.0 + */ + public function addAllowedObjectDomain($domain) { + $this->allowedObjectDomains[] = $domain; + return $this; + } + + /** + * Remove the specified allowed object domain from the allowed domains. + * + * @param string $domain + * @return $this + * @since 8.1.0 + */ + public function disallowObjectDomain($domain) { + $this->allowedObjectDomains = array_diff($this->allowedObjectDomains, [$domain]); + return $this; + } + + /** + * Which domains can be embedded in an iframe + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 8.1.0 + */ + public function addAllowedFrameDomain($domain) { + $this->allowedFrameDomains[] = $domain; + return $this; + } + + /** + * Remove the specified allowed frame domain from the allowed domains. + * + * @param string $domain + * @return $this + * @since 8.1.0 + */ + public function disallowFrameDomain($domain) { + $this->allowedFrameDomains = array_diff($this->allowedFrameDomains, [$domain]); + return $this; + } + + /** + * Domains from which web-workers and nested browsing content can load elements + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 8.1.0 + * @deprecated 15.0.0 use addAllowedWorkerSrcDomains or addAllowedFrameDomain + */ + public function addAllowedChildSrcDomain($domain) { + $this->allowedChildSrcDomains[] = $domain; + return $this; + } + + /** + * Remove the specified allowed child src domain from the allowed domains. + * + * @param string $domain + * @return $this + * @since 8.1.0 + * @deprecated 15.0.0 use the WorkerSrcDomains or FrameDomain + */ + public function disallowChildSrcDomain($domain) { + $this->allowedChildSrcDomains = array_diff($this->allowedChildSrcDomains, [$domain]); + return $this; + } + + /** + * Domains which can embed an iFrame of the Nextcloud instance + * + * @param string $domain + * @return $this + * @since 13.0.0 + */ + public function addAllowedFrameAncestorDomain($domain) { + $this->allowedFrameAncestors[] = $domain; + return $this; + } + + /** + * Domains which can embed an iFrame of the Nextcloud instance + * + * @param string $domain + * @return $this + * @since 13.0.0 + */ + public function disallowFrameAncestorDomain($domain) { + $this->allowedFrameAncestors = array_diff($this->allowedFrameAncestors, [$domain]); + return $this; + } + + /** + * Domain from which workers can be loaded + * + * @param string $domain + * @return $this + * @since 15.0.0 + */ + public function addAllowedWorkerSrcDomain(string $domain) { + $this->allowedWorkerSrcDomains[] = $domain; + return $this; + } + + /** + * Remove domain from which workers can be loaded + * + * @param string $domain + * @return $this + * @since 15.0.0 + */ + public function disallowWorkerSrcDomain(string $domain) { + $this->allowedWorkerSrcDomains = array_diff($this->allowedWorkerSrcDomains, [$domain]); + return $this; + } + + /** + * Domain to where forms can submit + * + * @since 17.0.0 + * + * @return $this + */ + public function addAllowedFormActionDomain(string $domain) { + $this->allowedFormActionDomains[] = $domain; + return $this; + } + + /** + * Remove domain to where forms can submit + * + * @return $this + * @since 17.0.0 + */ + public function disallowFormActionDomain(string $domain) { + $this->allowedFormActionDomains = array_diff($this->allowedFormActionDomains, [$domain]); + return $this; + } + + /** + * Add location to report CSP violations to + * + * @param string $location + * @return $this + * @since 15.0.0 + */ + public function addReportTo(string $location) { + $this->reportTo[] = $location; + return $this; + } + + /** + * Get the generated Content-Security-Policy as a string + * @return string + * @since 8.1.0 + */ + public function buildPolicy() { + $policy = "default-src 'none';"; + $policy .= "base-uri 'none';"; + $policy .= "manifest-src 'self';"; + + if (!empty($this->allowedScriptDomains) || $this->evalScriptAllowed || $this->evalWasmAllowed || is_string($this->jsNonce)) { + $policy .= 'script-src '; + $scriptSrc = ''; + if (is_string($this->jsNonce)) { + if ($this->strictDynamicAllowed) { + $scriptSrc .= '\'strict-dynamic\' '; + } + $scriptSrc .= '\'nonce-' . $this->jsNonce . '\''; + $allowedScriptDomains = array_flip($this->allowedScriptDomains); + unset($allowedScriptDomains['\'self\'']); + $this->allowedScriptDomains = array_flip($allowedScriptDomains); + if (count($allowedScriptDomains) !== 0) { + $scriptSrc .= ' '; + } + } + if (is_array($this->allowedScriptDomains)) { + $scriptSrc .= implode(' ', $this->allowedScriptDomains); + } + if ($this->evalScriptAllowed) { + $scriptSrc .= ' \'unsafe-eval\''; + } + if ($this->evalWasmAllowed) { + $scriptSrc .= ' \'wasm-unsafe-eval\''; + } + $policy .= $scriptSrc . ';'; + } + + // We only need to set this if 'strictDynamicAllowed' is not set because otherwise we can simply fall back to script-src + if ($this->strictDynamicAllowedOnScripts && is_string($this->jsNonce) && !$this->strictDynamicAllowed) { + $policy .= 'script-src-elem \'strict-dynamic\' '; + $policy .= $scriptSrc ?? ''; + $policy .= ';'; + } + + if (!empty($this->allowedStyleDomains) || $this->inlineStyleAllowed) { + $policy .= 'style-src '; + if (is_array($this->allowedStyleDomains)) { + $policy .= implode(' ', $this->allowedStyleDomains); + } + if ($this->inlineStyleAllowed) { + $policy .= ' \'unsafe-inline\''; + } + $policy .= ';'; + } + + if (!empty($this->allowedImageDomains)) { + $policy .= 'img-src ' . implode(' ', $this->allowedImageDomains); + $policy .= ';'; + } + + if (!empty($this->allowedFontDomains)) { + $policy .= 'font-src ' . implode(' ', $this->allowedFontDomains); + $policy .= ';'; + } + + if (!empty($this->allowedConnectDomains)) { + $policy .= 'connect-src ' . implode(' ', $this->allowedConnectDomains); + $policy .= ';'; + } + + if (!empty($this->allowedMediaDomains)) { + $policy .= 'media-src ' . implode(' ', $this->allowedMediaDomains); + $policy .= ';'; + } + + if (!empty($this->allowedObjectDomains)) { + $policy .= 'object-src ' . implode(' ', $this->allowedObjectDomains); + $policy .= ';'; + } + + if (!empty($this->allowedFrameDomains)) { + $policy .= 'frame-src '; + $policy .= implode(' ', $this->allowedFrameDomains); + $policy .= ';'; + } + + if (!empty($this->allowedChildSrcDomains)) { + $policy .= 'child-src ' . implode(' ', $this->allowedChildSrcDomains); + $policy .= ';'; + } + + if (!empty($this->allowedFrameAncestors)) { + $policy .= 'frame-ancestors ' . implode(' ', $this->allowedFrameAncestors); + $policy .= ';'; + } else { + $policy .= 'frame-ancestors \'none\';'; + } + + if (!empty($this->allowedWorkerSrcDomains)) { + $policy .= 'worker-src ' . implode(' ', $this->allowedWorkerSrcDomains); + $policy .= ';'; + } + + if (!empty($this->allowedFormActionDomains)) { + $policy .= 'form-action ' . implode(' ', $this->allowedFormActionDomains); + $policy .= ';'; + } + + if (!empty($this->reportTo)) { + $policy .= 'report-uri ' . implode(' ', $this->reportTo); + $policy .= ';'; + } + + return rtrim($policy, ';'); + } +} diff --git a/lib/public/AppFramework/Http/EmptyFeaturePolicy.php b/lib/public/AppFramework/Http/EmptyFeaturePolicy.php new file mode 100644 index 00000000000..a1d19a9f34b --- /dev/null +++ b/lib/public/AppFramework/Http/EmptyFeaturePolicy.php @@ -0,0 +1,164 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Http; + +/** + * Class EmptyFeaturePolicy is a simple helper which allows applications + * to modify the FeaturePolicy sent by Nextcloud. Per default the policy + * is forbidding everything. + * + * As alternative with sane exemptions look at FeaturePolicy + * + * @see \OCP\AppFramework\Http\FeaturePolicy + * @since 17.0.0 + */ +class EmptyFeaturePolicy { + /** @var string[] of allowed domains to autoplay media */ + protected $autoplayDomains = null; + + /** @var string[] of allowed domains that can access the camera */ + protected $cameraDomains = null; + + /** @var string[] of allowed domains that can use fullscreen */ + protected $fullscreenDomains = null; + + /** @var string[] of allowed domains that can use the geolocation of the device */ + protected $geolocationDomains = null; + + /** @var string[] of allowed domains that can use the microphone */ + protected $microphoneDomains = null; + + /** @var string[] of allowed domains that can use the payment API */ + protected $paymentDomains = null; + + /** + * Allows to use autoplay from a specific domain. Use * to allow from all domains. + * + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 17.0.0 + */ + public function addAllowedAutoplayDomain(string $domain): self { + $this->autoplayDomains[] = $domain; + return $this; + } + + /** + * Allows to use the camera on a specific domain. Use * to allow from all domains + * + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 17.0.0 + */ + public function addAllowedCameraDomain(string $domain): self { + $this->cameraDomains[] = $domain; + return $this; + } + + /** + * Allows the full screen functionality to be used on a specific domain. Use * to allow from all domains + * + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 17.0.0 + */ + public function addAllowedFullScreenDomain(string $domain): self { + $this->fullscreenDomains[] = $domain; + return $this; + } + + /** + * Allows to use the geolocation on a specific domain. Use * to allow from all domains + * + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 17.0.0 + */ + public function addAllowedGeoLocationDomain(string $domain): self { + $this->geolocationDomains[] = $domain; + return $this; + } + + /** + * Allows to use the microphone on a specific domain. Use * to allow from all domains + * + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 17.0.0 + */ + public function addAllowedMicrophoneDomain(string $domain): self { + $this->microphoneDomains[] = $domain; + return $this; + } + + /** + * Allows to use the payment API on a specific domain. Use * to allow from all domains + * + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 17.0.0 + */ + public function addAllowedPaymentDomain(string $domain): self { + $this->paymentDomains[] = $domain; + return $this; + } + + /** + * Get the generated Feature-Policy as a string + * + * @return string + * @since 17.0.0 + */ + public function buildPolicy(): string { + $policy = ''; + + if (empty($this->autoplayDomains)) { + $policy .= "autoplay 'none';"; + } else { + $policy .= 'autoplay ' . implode(' ', $this->autoplayDomains); + $policy .= ';'; + } + + if (empty($this->cameraDomains)) { + $policy .= "camera 'none';"; + } else { + $policy .= 'camera ' . implode(' ', $this->cameraDomains); + $policy .= ';'; + } + + if (empty($this->fullscreenDomains)) { + $policy .= "fullscreen 'none';"; + } else { + $policy .= 'fullscreen ' . implode(' ', $this->fullscreenDomains); + $policy .= ';'; + } + + if (empty($this->geolocationDomains)) { + $policy .= "geolocation 'none';"; + } else { + $policy .= 'geolocation ' . implode(' ', $this->geolocationDomains); + $policy .= ';'; + } + + if (empty($this->microphoneDomains)) { + $policy .= "microphone 'none';"; + } else { + $policy .= 'microphone ' . implode(' ', $this->microphoneDomains); + $policy .= ';'; + } + + if (empty($this->paymentDomains)) { + $policy .= "payment 'none';"; + } else { + $policy .= 'payment ' . implode(' ', $this->paymentDomains); + $policy .= ';'; + } + + return rtrim($policy, ';'); + } +} diff --git a/lib/public/AppFramework/Http/Events/BeforeLoginTemplateRenderedEvent.php b/lib/public/AppFramework/Http/Events/BeforeLoginTemplateRenderedEvent.php new file mode 100644 index 00000000000..b724b3a72ad --- /dev/null +++ b/lib/public/AppFramework/Http/Events/BeforeLoginTemplateRenderedEvent.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Http\Events; + +use OCP\AppFramework\Http\TemplateResponse; +use OCP\EventDispatcher\Event; + +/** + * Emitted before the rendering step of the login TemplateResponse. + * + * @since 28.0.0 + */ +class BeforeLoginTemplateRenderedEvent extends Event { + /** + * @since 28.0.0 + */ + public function __construct( + private TemplateResponse $response, + ) { + parent::__construct(); + } + + /** + * @since 28.0.0 + */ + public function getResponse(): TemplateResponse { + return $this->response; + } +} diff --git a/lib/public/AppFramework/Http/Events/BeforeTemplateRenderedEvent.php b/lib/public/AppFramework/Http/Events/BeforeTemplateRenderedEvent.php new file mode 100644 index 00000000000..7219ca5bfb6 --- /dev/null +++ b/lib/public/AppFramework/Http/Events/BeforeTemplateRenderedEvent.php @@ -0,0 +1,49 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Http\Events; + +use OCP\AppFramework\Http\TemplateResponse; +use OCP\EventDispatcher\Event; + +/** + * Emitted before the rendering step of each TemplateResponse. The event holds a + * flag that specifies if an user is logged in. + * + * @since 20.0.0 + */ +class BeforeTemplateRenderedEvent extends Event { + /** @var bool */ + private $loggedIn; + /** @var TemplateResponse */ + private $response; + + /** + * @since 20.0.0 + */ + public function __construct(bool $loggedIn, TemplateResponse $response) { + parent::__construct(); + + $this->loggedIn = $loggedIn; + $this->response = $response; + } + + /** + * @since 20.0.0 + */ + public function isLoggedIn(): bool { + return $this->loggedIn; + } + + /** + * @since 20.0.0 + */ + public function getResponse(): TemplateResponse { + return $this->response; + } +} diff --git a/lib/public/AppFramework/Http/FeaturePolicy.php b/lib/public/AppFramework/Http/FeaturePolicy.php new file mode 100644 index 00000000000..2291a78055c --- /dev/null +++ b/lib/public/AppFramework/Http/FeaturePolicy.php @@ -0,0 +1,41 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Http; + +/** + * Class FeaturePolicy is a simple helper which allows applications to + * modify the Feature-Policy sent by Nextcloud. Per default only autoplay is allowed + * from the same domain and full screen as well from the same domain. + * + * Even if a value gets modified above defaults will still get appended. Please + * notice that Nextcloud ships already with sensible defaults and those policies + * should require no modification at all for most use-cases. + * + * @since 17.0.0 + */ +class FeaturePolicy extends EmptyFeaturePolicy { + protected $autoplayDomains = [ + '\'self\'', + ]; + + /** @var string[] of allowed domains that can access the camera */ + protected $cameraDomains = []; + + protected $fullscreenDomains = [ + '\'self\'', + ]; + + /** @var string[] of allowed domains that can use the geolocation of the device */ + protected $geolocationDomains = []; + + /** @var string[] of allowed domains that can use the microphone */ + protected $microphoneDomains = []; + + /** @var string[] of allowed domains that can use the payment API */ + protected $paymentDomains = []; +} diff --git a/lib/public/AppFramework/Http/FileDisplayResponse.php b/lib/public/AppFramework/Http/FileDisplayResponse.php new file mode 100644 index 00000000000..c18404b7d91 --- /dev/null +++ b/lib/public/AppFramework/Http/FileDisplayResponse.php @@ -0,0 +1,55 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Http; + +use OCP\AppFramework\Http; +use OCP\Files\File; +use OCP\Files\SimpleFS\ISimpleFile; + +/** + * Class FileDisplayResponse + * + * @since 11.0.0 + * @template S of Http::STATUS_* + * @template H of array<string, mixed> + * @template-extends Response<Http::STATUS_*, array<string, mixed>> + */ +class FileDisplayResponse extends Response implements ICallbackResponse { + /** @var File|ISimpleFile */ + private $file; + + /** + * FileDisplayResponse constructor. + * + * @param File|ISimpleFile $file + * @param S $statusCode + * @param H $headers + * @since 11.0.0 + */ + public function __construct(File|ISimpleFile $file, int $statusCode = Http::STATUS_OK, array $headers = []) { + parent::__construct($statusCode, $headers); + + $this->file = $file; + $this->addHeader('Content-Disposition', 'inline; filename="' . rawurldecode($file->getName()) . '"'); + + $this->setETag($file->getEtag()); + $lastModified = new \DateTime(); + $lastModified->setTimestamp($file->getMTime()); + $this->setLastModified($lastModified); + } + + /** + * @param IOutput $output + * @since 11.0.0 + */ + public function callback(IOutput $output) { + if ($output->getHttpResponseCode() !== Http::STATUS_NOT_MODIFIED) { + $output->setHeader('Content-Length: ' . $this->file->getSize()); + $output->setOutput($this->file->getContent()); + } + } +} diff --git a/lib/public/AppFramework/Http/ICallbackResponse.php b/lib/public/AppFramework/Http/ICallbackResponse.php new file mode 100644 index 00000000000..a51f72612fb --- /dev/null +++ b/lib/public/AppFramework/Http/ICallbackResponse.php @@ -0,0 +1,23 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCP\AppFramework\Http; + +/** + * Interface ICallbackResponse + * + * @since 8.1.0 + */ +interface ICallbackResponse { + /** + * Outputs the content that should be printed + * + * @param IOutput $output a small wrapper that handles output + * @since 8.1.0 + */ + public function callback(IOutput $output); +} diff --git a/lib/public/AppFramework/Http/IOutput.php b/lib/public/AppFramework/Http/IOutput.php new file mode 100644 index 00000000000..105eaa0edb9 --- /dev/null +++ b/lib/public/AppFramework/Http/IOutput.php @@ -0,0 +1,59 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCP\AppFramework\Http; + +/** + * Very thin wrapper class to make output testable + * @since 8.1.0 + */ +interface IOutput { + /** + * @param string $out + * @since 8.1.0 + */ + public function setOutput($out); + + /** + * @param string|resource $path or file handle + * + * @return bool false if an error occurred + * @since 8.1.0 + */ + public function setReadfile($path); + + /** + * @param string $header + * @since 8.1.0 + */ + public function setHeader($header); + + /** + * @return int returns the current http response code + * @since 8.1.0 + */ + public function getHttpResponseCode(); + + /** + * @param int $code sets the http status code + * @since 8.1.0 + */ + public function setHttpResponseCode($code); + + /** + * @param string $name + * @param string $value + * @param int $expire + * @param string $path + * @param string $domain + * @param bool $secure + * @param bool $httpOnly + * @param string $sameSite (added in 20) + * @since 8.1.0 + */ + public function setCookie($name, $value, $expire, $path, $domain, $secure, $httpOnly, $sameSite = 'Lax'); +} diff --git a/lib/public/AppFramework/Http/JSONResponse.php b/lib/public/AppFramework/Http/JSONResponse.php new file mode 100644 index 00000000000..a226e29a1b5 --- /dev/null +++ b/lib/public/AppFramework/Http/JSONResponse.php @@ -0,0 +1,91 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCP\AppFramework\Http; + +use OCP\AppFramework\Http; + +/** + * A renderer for JSON calls + * @since 6.0.0 + * @template S of Http::STATUS_* + * @template-covariant T of null|string|int|float|bool|array|\stdClass|\JsonSerializable + * @template H of array<string, mixed> + * @template-extends Response<Http::STATUS_*, array<string, mixed>> + */ +class JSONResponse extends Response { + /** + * response data + * @var T + */ + protected $data; + /** + * Additional `json_encode` flags + * @var int + */ + protected $encodeFlags; + + + /** + * constructor of JSONResponse + * @param T $data the object or array that should be transformed + * @param S $statusCode the Http status code, defaults to 200 + * @param H $headers + * @param int $encodeFlags Additional `json_encode` flags + * @since 6.0.0 + * @since 30.0.0 Added `$encodeFlags` param + */ + public function __construct( + mixed $data = [], + int $statusCode = Http::STATUS_OK, + array $headers = [], + int $encodeFlags = 0, + ) { + parent::__construct($statusCode, $headers); + + $this->data = $data; + $this->encodeFlags = $encodeFlags; + $this->addHeader('Content-Type', 'application/json; charset=utf-8'); + } + + + /** + * Returns the rendered json + * @return string the rendered json + * @since 6.0.0 + * @throws \Exception If data could not get encoded + * + * @psalm-taint-escape has_quotes + * @psalm-taint-escape html + */ + public function render() { + return json_encode($this->data, JSON_HEX_TAG | JSON_THROW_ON_ERROR | $this->encodeFlags, 2048); + } + + /** + * Sets values in the data json array + * @psalm-suppress InvalidTemplateParam + * @param T $data an array or object which will be transformed + * to JSON + * @return JSONResponse Reference to this object + * @since 6.0.0 - return value was added in 7.0.0 + */ + public function setData($data) { + $this->data = $data; + + return $this; + } + + + /** + * @return T the data + * @since 6.0.0 + */ + public function getData() { + return $this->data; + } +} diff --git a/lib/public/AppFramework/Http/NotFoundResponse.php b/lib/public/AppFramework/Http/NotFoundResponse.php new file mode 100644 index 00000000000..137d1a26655 --- /dev/null +++ b/lib/public/AppFramework/Http/NotFoundResponse.php @@ -0,0 +1,30 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCP\AppFramework\Http; + +use OCP\AppFramework\Http; + +/** + * A generic 404 response showing an 404 error page as well to the end-user + * @since 8.1.0 + * @template S of Http::STATUS_* + * @template H of array<string, mixed> + * @template-extends TemplateResponse<Http::STATUS_*, array<string, mixed>> + */ +class NotFoundResponse extends TemplateResponse { + /** + * @param S $status + * @param H $headers + * @since 8.1.0 + */ + public function __construct(int $status = Http::STATUS_NOT_FOUND, array $headers = []) { + parent::__construct('core', '404', [], 'guest', $status, $headers); + + $this->setContentSecurityPolicy(new ContentSecurityPolicy()); + } +} diff --git a/lib/public/AppFramework/Http/ParameterOutOfRangeException.php b/lib/public/AppFramework/Http/ParameterOutOfRangeException.php new file mode 100644 index 00000000000..3286917d4d0 --- /dev/null +++ b/lib/public/AppFramework/Http/ParameterOutOfRangeException.php @@ -0,0 +1,62 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\AppFramework\Http; + +/** + * @since 29.0.0 + */ +class ParameterOutOfRangeException extends \OutOfRangeException { + /** + * @since 29.0.0 + */ + public function __construct( + protected string $parameterName, + protected int $actualValue, + protected int $minValue, + protected int $maxValue, + ) { + parent::__construct( + sprintf( + 'Parameter %s must be between %d and %d', + $this->parameterName, + $this->minValue, + $this->maxValue, + ) + ); + } + + /** + * @since 29.0.0 + */ + public function getParameterName(): string { + return $this->parameterName; + } + + /** + * @since 29.0.0 + */ + public function getActualValue(): int { + return $this->actualValue; + } + + /** + * @since 29.0.0 + */ + public function getMinValue(): int { + return $this->minValue; + } + + /** + * @since 29.0.0 + */ + public function getMaxValue(): int { + return $this->maxValue; + } +} diff --git a/lib/public/AppFramework/Http/RedirectResponse.php b/lib/public/AppFramework/Http/RedirectResponse.php new file mode 100644 index 00000000000..74847205976 --- /dev/null +++ b/lib/public/AppFramework/Http/RedirectResponse.php @@ -0,0 +1,44 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCP\AppFramework\Http; + +use OCP\AppFramework\Http; + +/** + * Redirects to a different URL + * @since 7.0.0 + * @template S of Http::STATUS_* + * @template H of array<string, mixed> + * @template-extends Response<Http::STATUS_*, array<string, mixed>> + */ +class RedirectResponse extends Response { + private $redirectURL; + + /** + * Creates a response that redirects to a url + * @param string $redirectURL the url to redirect to + * @param S $status + * @param H $headers + * @since 7.0.0 + */ + public function __construct(string $redirectURL, int $status = Http::STATUS_SEE_OTHER, array $headers = []) { + parent::__construct($status, $headers); + + $this->redirectURL = $redirectURL; + $this->addHeader('Location', $redirectURL); + } + + + /** + * @return string the url to redirect + * @since 7.0.0 + */ + public function getRedirectURL() { + return $this->redirectURL; + } +} diff --git a/lib/public/AppFramework/Http/RedirectToDefaultAppResponse.php b/lib/public/AppFramework/Http/RedirectToDefaultAppResponse.php new file mode 100644 index 00000000000..0a0c04f671d --- /dev/null +++ b/lib/public/AppFramework/Http/RedirectToDefaultAppResponse.php @@ -0,0 +1,36 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Http; + +use OCP\AppFramework\Http; +use OCP\IURLGenerator; + +/** + * Redirects to the default app + * + * @since 16.0.0 + * @deprecated 23.0.0 Use RedirectResponse() with IURLGenerator::linkToDefaultPageUrl() instead + * @template S of Http::STATUS_* + * @template H of array<string, mixed> + * @template-extends RedirectResponse<Http::STATUS_*, array<string, mixed>> + */ +class RedirectToDefaultAppResponse extends RedirectResponse { + /** + * Creates a response that redirects to the default app + * + * @param S $status + * @param H $headers + * @since 16.0.0 + * @deprecated 23.0.0 Use RedirectResponse() with IURLGenerator::linkToDefaultPageUrl() instead + */ + public function __construct(int $status = Http::STATUS_SEE_OTHER, array $headers = []) { + $urlGenerator = \OCP\Server::get(IURLGenerator::class); + parent::__construct($urlGenerator->linkToDefaultPageUrl(), $status, $headers); + } +} diff --git a/lib/public/AppFramework/Http/Response.php b/lib/public/AppFramework/Http/Response.php new file mode 100644 index 00000000000..bdebb12c00d --- /dev/null +++ b/lib/public/AppFramework/Http/Response.php @@ -0,0 +1,408 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCP\AppFramework\Http; + +use OCP\AppFramework\Http; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; +use OCP\IRequest; +use Psr\Log\LoggerInterface; + +/** + * Base class for responses. Also used to just send headers. + * + * It handles headers, HTTP status code, last modified and ETag. + * @since 6.0.0 + * @template S of Http::STATUS_* + * @template H of array<string, mixed> + */ +class Response { + /** + * Headers + * @var H + */ + private $headers; + + + /** + * Cookies that will be need to be constructed as header + * @var array + */ + private $cookies = []; + + + /** + * HTTP status code - defaults to STATUS OK + * @var S + */ + private $status; + + + /** + * Last modified date + * @var \DateTime + */ + private $lastModified; + + + /** + * ETag + * @var string + */ + private $ETag; + + /** @var ContentSecurityPolicy|null Used Content-Security-Policy */ + private $contentSecurityPolicy = null; + + /** @var FeaturePolicy */ + private $featurePolicy; + + /** @var bool */ + private $throttled = false; + /** @var array */ + private $throttleMetadata = []; + + /** + * @param S $status + * @param H $headers + * @since 17.0.0 + */ + public function __construct(int $status = Http::STATUS_OK, array $headers = []) { + $this->setStatus($status); + $this->setHeaders($headers); + } + + /** + * Caches the response + * + * @param int $cacheSeconds amount of seconds the response is fresh, 0 to disable cache. + * @param bool $public whether the page should be cached by public proxy. Usually should be false, unless this is a static resources. + * @param bool $immutable whether browser should treat the resource as immutable and not ask the server for each page load if the resource changed. + * @return $this + * @since 6.0.0 - return value was added in 7.0.0 + */ + public function cacheFor(int $cacheSeconds, bool $public = false, bool $immutable = false) { + if ($cacheSeconds > 0) { + $cacheStore = $public ? 'public' : 'private'; + $this->addHeader('Cache-Control', sprintf('%s, max-age=%s, %s', $cacheStore, $cacheSeconds, ($immutable ? 'immutable' : 'must-revalidate'))); + + // Set expires header + $expires = new \DateTime(); + $time = \OCP\Server::get(ITimeFactory::class); + $expires->setTimestamp($time->getTime()); + $expires->add(new \DateInterval('PT' . $cacheSeconds . 'S')); + $this->addHeader('Expires', $expires->format(\DateTimeInterface::RFC7231)); + } else { + $this->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + unset($this->headers['Expires']); + } + + return $this; + } + + /** + * Adds a new cookie to the response + * @param string $name The name of the cookie + * @param string $value The value of the cookie + * @param \DateTime|null $expireDate Date on that the cookie should expire, if set + * to null cookie will be considered as session + * cookie. + * @param string $sameSite The samesite value of the cookie. Defaults to Lax. Other possibilities are Strict or None + * @return $this + * @since 8.0.0 + */ + public function addCookie($name, $value, ?\DateTime $expireDate = null, $sameSite = 'Lax') { + $this->cookies[$name] = ['value' => $value, 'expireDate' => $expireDate, 'sameSite' => $sameSite]; + return $this; + } + + + /** + * Set the specified cookies + * @param array $cookies array('foo' => array('value' => 'bar', 'expire' => null)) + * @return $this + * @since 8.0.0 + */ + public function setCookies(array $cookies) { + $this->cookies = $cookies; + return $this; + } + + + /** + * Invalidates the specified cookie + * @param string $name + * @return $this + * @since 8.0.0 + */ + public function invalidateCookie($name) { + $this->addCookie($name, 'expired', new \DateTime('1971-01-01 00:00')); + return $this; + } + + /** + * Invalidates the specified cookies + * @param array $cookieNames array('foo', 'bar') + * @return $this + * @since 8.0.0 + */ + public function invalidateCookies(array $cookieNames) { + foreach ($cookieNames as $cookieName) { + $this->invalidateCookie($cookieName); + } + return $this; + } + + /** + * Returns the cookies + * @return array + * @since 8.0.0 + */ + public function getCookies() { + return $this->cookies; + } + + /** + * Adds a new header to the response that will be called before the render + * function + * @param string $name The name of the HTTP header + * @param string $value The value, null will delete it + * @return $this + * @since 6.0.0 - return value was added in 7.0.0 + */ + public function addHeader($name, $value) { + $name = trim($name); // always remove leading and trailing whitespace + // to be able to reliably check for security + // headers + + if ($this->status === Http::STATUS_NOT_MODIFIED + && stripos($name, 'x-') === 0) { + /** @var IConfig $config */ + $config = \OCP\Server::get(IConfig::class); + + if ($config->getSystemValueBool('debug', false)) { + \OCP\Server::get(LoggerInterface::class)->error('Setting custom header on a 304 is not supported (Header: {header})', [ + 'header' => $name, + ]); + } + } + + if (is_null($value)) { + unset($this->headers[$name]); + } else { + $this->headers[$name] = $value; + } + + return $this; + } + + + /** + * Set the headers + * @template NewH as array<string, mixed> + * @param NewH $headers value header pairs + * @psalm-this-out static<S, NewH> + * @return static + * @since 8.0.0 + */ + public function setHeaders(array $headers): static { + /** @psalm-suppress InvalidPropertyAssignmentValue Expected due to @psalm-this-out */ + $this->headers = $headers; + + return $this; + } + + + /** + * Returns the set headers + * @return array{X-Request-Id: string, Cache-Control: string, Content-Security-Policy: string, Feature-Policy: string, X-Robots-Tag: string, Last-Modified?: string, ETag?: string, ...H} the headers + * @since 6.0.0 + */ + public function getHeaders() { + /** @var IRequest $request */ + /** + * @psalm-suppress UndefinedClass + */ + $request = \OCP\Server::get(IRequest::class); + $mergeWith = [ + 'X-Request-Id' => $request->getId(), + 'Cache-Control' => 'no-cache, no-store, must-revalidate', + 'Content-Security-Policy' => $this->getContentSecurityPolicy()->buildPolicy(), + 'Feature-Policy' => $this->getFeaturePolicy()->buildPolicy(), + 'X-Robots-Tag' => 'noindex, nofollow', + ]; + + if ($this->lastModified) { + $mergeWith['Last-Modified'] = $this->lastModified->format(\DateTimeInterface::RFC7231); + } + + if ($this->ETag) { + $mergeWith['ETag'] = '"' . $this->ETag . '"'; + } + + return array_merge($mergeWith, $this->headers); + } + + + /** + * By default renders no output + * @return string + * @since 6.0.0 + */ + public function render() { + return ''; + } + + + /** + * Set response status + * @template NewS as int + * @param NewS $status a HTTP status code, see also the STATUS constants + * @psalm-this-out static<NewS, H> + * @return static + * @since 6.0.0 - return value was added in 7.0.0 + */ + public function setStatus($status): static { + /** @psalm-suppress InvalidPropertyAssignmentValue Expected due to @psalm-this-out */ + $this->status = $status; + + return $this; + } + + /** + * Set a Content-Security-Policy + * @param EmptyContentSecurityPolicy $csp Policy to set for the response object + * @return $this + * @since 8.1.0 + */ + public function setContentSecurityPolicy(EmptyContentSecurityPolicy $csp) { + $this->contentSecurityPolicy = $csp; + return $this; + } + + /** + * Get the currently used Content-Security-Policy + * @return EmptyContentSecurityPolicy|null Used Content-Security-Policy or null if + * none specified. + * @since 8.1.0 + */ + public function getContentSecurityPolicy() { + if ($this->contentSecurityPolicy === null) { + $this->setContentSecurityPolicy(new EmptyContentSecurityPolicy()); + } + return $this->contentSecurityPolicy; + } + + + /** + * @since 17.0.0 + */ + public function getFeaturePolicy(): EmptyFeaturePolicy { + if ($this->featurePolicy === null) { + $this->setFeaturePolicy(new EmptyFeaturePolicy()); + } + return $this->featurePolicy; + } + + /** + * @since 17.0.0 + */ + public function setFeaturePolicy(EmptyFeaturePolicy $featurePolicy): self { + $this->featurePolicy = $featurePolicy; + + return $this; + } + + + + /** + * Get response status + * @since 6.0.0 + * @return S + */ + public function getStatus() { + return $this->status; + } + + + /** + * Get the ETag + * @return string the etag + * @since 6.0.0 + */ + public function getETag() { + return $this->ETag; + } + + + /** + * Get "last modified" date + * @return \DateTime RFC2822 formatted last modified date + * @since 6.0.0 + */ + public function getLastModified() { + return $this->lastModified; + } + + + /** + * Set the ETag + * @param string $ETag + * @return Response Reference to this object + * @since 6.0.0 - return value was added in 7.0.0 + */ + public function setETag($ETag) { + $this->ETag = $ETag; + + return $this; + } + + + /** + * Set "last modified" date + * @param \DateTime $lastModified + * @return Response Reference to this object + * @since 6.0.0 - return value was added in 7.0.0 + */ + public function setLastModified($lastModified) { + $this->lastModified = $lastModified; + + return $this; + } + + /** + * Marks the response as to throttle. Will be throttled when the + * @BruteForceProtection annotation is added. + * + * @param array $metadata + * @since 12.0.0 + */ + public function throttle(array $metadata = []) { + $this->throttled = true; + $this->throttleMetadata = $metadata; + } + + /** + * Returns the throttle metadata, defaults to empty array + * + * @return array + * @since 13.0.0 + */ + public function getThrottleMetadata() { + return $this->throttleMetadata; + } + + /** + * Whether the current response is throttled. + * + * @since 12.0.0 + */ + public function isThrottled() { + return $this->throttled; + } +} diff --git a/lib/public/AppFramework/Http/StandaloneTemplateResponse.php b/lib/public/AppFramework/Http/StandaloneTemplateResponse.php new file mode 100644 index 00000000000..244a6b80f9f --- /dev/null +++ b/lib/public/AppFramework/Http/StandaloneTemplateResponse.php @@ -0,0 +1,24 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Http; + +use OCP\AppFramework\Http; + +/** + * A template response that does not emit the loadAdditionalScripts events. + * + * This is useful for pages that are authenticated but do not yet show the + * full nextcloud UI. Like the 2FA page, or the grant page in the login flow. + * + * @since 16.0.0 + * @template S of Http::STATUS_* + * @template H of array<string, mixed> + * @template-extends TemplateResponse<Http::STATUS_*, array<string, mixed>> + */ +class StandaloneTemplateResponse extends TemplateResponse { +} diff --git a/lib/public/AppFramework/Http/StreamResponse.php b/lib/public/AppFramework/Http/StreamResponse.php new file mode 100644 index 00000000000..d0e6e3e148a --- /dev/null +++ b/lib/public/AppFramework/Http/StreamResponse.php @@ -0,0 +1,53 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCP\AppFramework\Http; + +use OCP\AppFramework\Http; + +/** + * Class StreamResponse + * + * @since 8.1.0 + * @template S of Http::STATUS_* + * @template H of array<string, mixed> + * @template-extends Response<Http::STATUS_*, array<string, mixed>> + */ +class StreamResponse extends Response implements ICallbackResponse { + /** @var string */ + private $filePath; + + /** + * @param string|resource $filePath the path to the file or a file handle which should be streamed + * @param S $status + * @param H $headers + * @since 8.1.0 + */ + public function __construct(mixed $filePath, int $status = Http::STATUS_OK, array $headers = []) { + parent::__construct($status, $headers); + + $this->filePath = $filePath; + } + + + /** + * Streams the file using readfile + * + * @param IOutput $output a small wrapper that handles output + * @since 8.1.0 + */ + public function callback(IOutput $output) { + // handle caching + if ($output->getHttpResponseCode() !== Http::STATUS_NOT_MODIFIED) { + if (!(is_resource($this->filePath) || file_exists($this->filePath))) { + $output->setHttpResponseCode(Http::STATUS_NOT_FOUND); + } elseif ($output->setReadfile($this->filePath) === false) { + $output->setHttpResponseCode(Http::STATUS_BAD_REQUEST); + } + } + } +} diff --git a/lib/public/AppFramework/Http/StrictContentSecurityPolicy.php b/lib/public/AppFramework/Http/StrictContentSecurityPolicy.php new file mode 100644 index 00000000000..4b074331fc8 --- /dev/null +++ b/lib/public/AppFramework/Http/StrictContentSecurityPolicy.php @@ -0,0 +1,70 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Http; + +/** + * Class StrictContentSecurityPolicy is a simple helper which allows applications to + * modify the Content-Security-Policy sent by Nextcloud. Per default only JavaScript, + * stylesheets, images, fonts, media and connections from the same domain + * ('self') are allowed. + * + * Even if a value gets modified above defaults will still get appended. Please + * note that Nextcloud ships already with sensible defaults and those policies + * should require no modification at all for most use-cases. + * + * This class represents out strictest defaults. They may get change from release + * to release if more strict CSP directives become available. + * + * @since 14.0.0 + * @deprecated 17.0.0 + */ +class StrictContentSecurityPolicy extends EmptyContentSecurityPolicy { + /** @var bool Whether inline JS snippets are allowed */ + protected $inlineScriptAllowed = false; + /** @var bool Whether eval in JS scripts is allowed */ + protected $evalScriptAllowed = false; + /** @var bool Whether WebAssembly compilation is allowed */ + protected ?bool $evalWasmAllowed = false; + /** @var array Domains from which scripts can get loaded */ + protected $allowedScriptDomains = [ + '\'self\'', + ]; + /** @var bool Whether inline CSS is allowed */ + protected $inlineStyleAllowed = false; + /** @var array Domains from which CSS can get loaded */ + protected $allowedStyleDomains = [ + '\'self\'', + ]; + /** @var array Domains from which images can get loaded */ + protected $allowedImageDomains = [ + '\'self\'', + 'data:', + 'blob:', + ]; + /** @var array Domains to which connections can be done */ + protected $allowedConnectDomains = [ + '\'self\'', + ]; + /** @var array Domains from which media elements can be loaded */ + protected $allowedMediaDomains = [ + '\'self\'', + ]; + /** @var array Domains from which object elements can be loaded */ + protected $allowedObjectDomains = []; + /** @var array Domains from which iframes can be loaded */ + protected $allowedFrameDomains = []; + /** @var array Domains from which fonts can be loaded */ + protected $allowedFontDomains = [ + '\'self\'', + ]; + /** @var array Domains from which web-workers and nested browsing content can load elements */ + protected $allowedChildSrcDomains = []; + + /** @var array Domains which can embed this Nextcloud instance */ + protected $allowedFrameAncestors = []; +} diff --git a/lib/public/AppFramework/Http/StrictEvalContentSecurityPolicy.php b/lib/public/AppFramework/Http/StrictEvalContentSecurityPolicy.php new file mode 100644 index 00000000000..b59dd0fcce7 --- /dev/null +++ b/lib/public/AppFramework/Http/StrictEvalContentSecurityPolicy.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Http; + +/** + * Class StrictEvalContentSecurityPolicy is a simple helper which allows applications to + * modify the Content-Security-Policy sent by Nextcloud. Per default only JavaScript, + * stylesheets, images, fonts, media and connections from the same domain + * ('self') are allowed. + * + * Even if a value gets modified above defaults will still get appended. Please + * note that Nextcloud ships already with sensible defaults and those policies + * should require no modification at all for most use-cases. + * + * This is a temp helper class from the default ContentSecurityPolicy to allow slow + * migration to a stricter CSP. This does not allow unsafe eval. + * + * @since 14.0.0 + * @deprecated 17.0.0 + */ +class StrictEvalContentSecurityPolicy extends ContentSecurityPolicy { + /** + * @since 14.0.0 + */ + public function __construct() { + $this->evalScriptAllowed = false; + } +} diff --git a/lib/public/AppFramework/Http/StrictInlineContentSecurityPolicy.php b/lib/public/AppFramework/Http/StrictInlineContentSecurityPolicy.php new file mode 100644 index 00000000000..e80d37c74cf --- /dev/null +++ b/lib/public/AppFramework/Http/StrictInlineContentSecurityPolicy.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Http; + +/** + * Class StrictInlineContentSecurityPolicy is a simple helper which allows applications to + * modify the Content-Security-Policy sent by Nextcloud. Per default only JavaScript, + * stylesheets, images, fonts, media and connections from the same domain + * ('self') are allowed. + * + * Even if a value gets modified above defaults will still get appended. Please + * note that Nextcloud ships already with sensible defaults and those policies + * should require no modification at all for most use-cases. + * + * This is a temp helper class from the default ContentSecurityPolicy to allow slow + * migration to a stricter CSP. This does not allow inline styles. + * + * @since 14.0.0 + * @deprecated 17.0.0 + */ +class StrictInlineContentSecurityPolicy extends ContentSecurityPolicy { + /** + * @since 14.0.0 + */ + public function __construct() { + $this->inlineStyleAllowed = false; + } +} diff --git a/lib/public/AppFramework/Http/Template/ExternalShareMenuAction.php b/lib/public/AppFramework/Http/Template/ExternalShareMenuAction.php new file mode 100644 index 00000000000..281bb559a10 --- /dev/null +++ b/lib/public/AppFramework/Http/Template/ExternalShareMenuAction.php @@ -0,0 +1,29 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Http\Template; + +/** + * Class LinkMenuAction + * + * @since 14.0.0 + */ +class ExternalShareMenuAction extends SimpleMenuAction { + + /** + * ExternalShareMenuAction constructor. + * + * @param string $label Translated label + * @param string $icon Icon CSS class + * @param string $owner Owner user ID (unused) + * @param string $displayname Display name of the owner (unused) + * @param string $shareName Name of the share (unused) + * @since 14.0.0 + */ + public function __construct(string $label, string $icon, string $owner, string $displayname, string $shareName) { + parent::__construct('save', $label, $icon); + } +} diff --git a/lib/public/AppFramework/Http/Template/IMenuAction.php b/lib/public/AppFramework/Http/Template/IMenuAction.php new file mode 100644 index 00000000000..124e95fe019 --- /dev/null +++ b/lib/public/AppFramework/Http/Template/IMenuAction.php @@ -0,0 +1,51 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Http\Template; + +/** + * Interface IMenuAction + * + * @since 14.0 + */ +interface IMenuAction { + /** + * @since 14.0.0 + * @return string + */ + public function getId(): string; + + /** + * The translated label of the menu item. + * + * @since 14.0.0 + * @return string + */ + public function getLabel(): string; + + /** + * The link this menu item points to. + * + * @since 14.0.0 + * @return string + */ + public function getLink(): string; + + /** + * @since 14.0.0 + * @return int + */ + public function getPriority(): int; + + /** + * Custom render function. + * The returned HTML will be wrapped within a listitem element (`<li>...</li>`). + * + * @since 14.0.0 + * @return string + */ + public function render(): string; +} diff --git a/lib/public/AppFramework/Http/Template/LinkMenuAction.php b/lib/public/AppFramework/Http/Template/LinkMenuAction.php new file mode 100644 index 00000000000..391802a1dce --- /dev/null +++ b/lib/public/AppFramework/Http/Template/LinkMenuAction.php @@ -0,0 +1,26 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Http\Template; + +/** + * Class LinkMenuAction + * + * @since 14.0.0 + */ +class LinkMenuAction extends SimpleMenuAction { + /** + * LinkMenuAction constructor. + * + * @param string $label + * @param string $icon + * @param string $link + * @since 14.0.0 + */ + public function __construct(string $label, string $icon, string $link) { + parent::__construct('directLink', $label, $icon, $link); + } +} diff --git a/lib/public/AppFramework/Http/Template/PublicTemplateResponse.php b/lib/public/AppFramework/Http/Template/PublicTemplateResponse.php new file mode 100644 index 00000000000..4c156cdecea --- /dev/null +++ b/lib/public/AppFramework/Http/Template/PublicTemplateResponse.php @@ -0,0 +1,176 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Http\Template; + +use InvalidArgumentException; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IInitialStateService; + +/** + * Class PublicTemplateResponse + * + * @since 14.0.0 + * @template H of array<string, mixed> + * @template S of Http::STATUS_* + * @template-extends TemplateResponse<Http::STATUS_*, array<string, mixed>> + */ +class PublicTemplateResponse extends TemplateResponse { + private $headerTitle = ''; + private $headerDetails = ''; + /** @var IMenuAction[] */ + private $headerActions = []; + private $footerVisible = true; + + /** + * PublicTemplateResponse constructor. + * + * @param string $appName + * @param string $templateName + * @param array $params + * @param S $status + * @param H $headers + * @since 14.0.0 + */ + public function __construct( + string $appName, + string $templateName, + array $params = [], + $status = Http::STATUS_OK, + array $headers = [], + ) { + parent::__construct($appName, $templateName, $params, 'public', $status, $headers); + \OCP\Util::addScript('core', 'public-page-menu'); + \OCP\Util::addScript('core', 'public-page-user-menu'); + + $state = \OCP\Server::get(IInitialStateService::class); + $state->provideLazyInitialState('core', 'public-page-menu', function () { + $response = []; + foreach ($this->headerActions as $action) { + // First try in it is a custom action that provides rendered HTML + $rendered = $action->render(); + if ($rendered === '') { + // If simple action, add the response data + if ($action instanceof SimpleMenuAction) { + $response[] = $action->getData(); + } + } else { + // custom action so add the rendered output + $response[] = [ + 'id' => $action->getId(), + 'label' => $action->getLabel(), + 'html' => $rendered, + ]; + } + } + return $response; + }); + } + + /** + * @param string $title + * @since 14.0.0 + */ + public function setHeaderTitle(string $title) { + $this->headerTitle = $title; + } + + /** + * @return string + * @since 14.0.0 + */ + public function getHeaderTitle(): string { + return $this->headerTitle; + } + + /** + * @param string $details + * @since 14.0.0 + */ + public function setHeaderDetails(string $details) { + $this->headerDetails = $details; + } + + /** + * @return string + * @since 14.0.0 + */ + public function getHeaderDetails(): string { + return $this->headerDetails; + } + + /** + * @param array $actions + * @since 14.0.0 + * @throws InvalidArgumentException + */ + public function setHeaderActions(array $actions) { + foreach ($actions as $action) { + if ($actions instanceof IMenuAction) { + throw new InvalidArgumentException('Actions must be of type IMenuAction'); + } + $this->headerActions[] = $action; + } + usort($this->headerActions, function (IMenuAction $a, IMenuAction $b) { + return $a->getPriority() <=> $b->getPriority(); + }); + } + + /** + * @return IMenuAction + * @since 14.0.0 + * @throws \Exception + */ + public function getPrimaryAction(): IMenuAction { + if ($this->getActionCount() > 0) { + return $this->headerActions[0]; + } + throw new \Exception('No header actions have been set'); + } + + /** + * @return int + * @since 14.0.0 + */ + public function getActionCount(): int { + return count($this->headerActions); + } + + /** + * @return IMenuAction[] + * @since 14.0.0 + */ + public function getOtherActions(): array { + return array_slice($this->headerActions, 1); + } + + /** + * @since 14.0.0 + */ + public function setFooterVisible(bool $visible = false) { + $this->footerVisible = $visible; + } + + /** + * @since 14.0.0 + */ + public function getFooterVisible(): bool { + return $this->footerVisible; + } + + /** + * @return string + * @since 14.0.0 + */ + public function render(): string { + $params = array_merge($this->getParams(), [ + 'template' => $this, + ]); + $this->setParams($params); + return parent::render(); + } +} diff --git a/lib/public/AppFramework/Http/Template/SimpleMenuAction.php b/lib/public/AppFramework/Http/Template/SimpleMenuAction.php new file mode 100644 index 00000000000..03cb9b4c7ea --- /dev/null +++ b/lib/public/AppFramework/Http/Template/SimpleMenuAction.php @@ -0,0 +1,120 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Http\Template; + +/** + * Class SimpleMenuAction + * + * @since 14.0.0 + */ +class SimpleMenuAction implements IMenuAction { + /** @var string */ + private $id; + + /** @var string */ + private $label; + + /** @var string */ + private $icon; + + /** @var string */ + private $link; + + /** @var int */ + private $priority; + + /** @var string */ + private $detail; + + /** + * SimpleMenuAction constructor. + * + * @param string $id + * @param string $label + * @param string $icon + * @param string $link + * @param int $priority + * @param string $detail + * @since 14.0.0 + */ + public function __construct(string $id, string $label, string $icon, string $link = '', int $priority = 100, string $detail = '') { + $this->id = $id; + $this->label = $label; + $this->icon = $icon; + $this->link = $link; + $this->priority = $priority; + $this->detail = $detail; + } + + /** + * @return string + * @since 14.0.0 + */ + public function getId(): string { + return $this->id; + } + + /** + * @return string + * @since 14.0.0 + */ + public function getLabel(): string { + return $this->label; + } + + /** + * The icon CSS class to use. + * + * @return string + * @since 14.0.0 + */ + public function getIcon(): string { + return $this->icon; + } + + /** + * @return string + * @since 14.0.0 + */ + public function getLink(): string { + return $this->link; + } + + /** + * @return int + * @since 14.0.0 + */ + public function getPriority(): int { + return $this->priority; + } + + /** + * Custom render function. + * The returned HTML must be wrapped within a listitem (`<li>...</li>`). + * * If an empty string is returned, the default design is used (based on the label and link specified). + * @return string + * @since 14.0.0 + */ + public function render(): string { + return ''; + } + + /** + * Return JSON data to let the frontend render the menu entry. + * @return array{id: string, label: string, href: string, icon: string, details: string|null} + * @since 31.0.0 + */ + public function getData(): array { + return [ + 'id' => $this->id, + 'label' => $this->label, + 'href' => $this->link, + 'icon' => $this->icon, + 'details' => $this->detail, + ]; + } +} diff --git a/lib/public/AppFramework/Http/TemplateResponse.php b/lib/public/AppFramework/Http/TemplateResponse.php new file mode 100644 index 00000000000..af37a1a2313 --- /dev/null +++ b/lib/public/AppFramework/Http/TemplateResponse.php @@ -0,0 +1,197 @@ +<?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 OCP\AppFramework\Http; + +use OCP\AppFramework\Http; +use OCP\Server; +use OCP\Template\ITemplateManager; + +/** + * Response for a normal template + * @since 6.0.0 + * + * @template S of Http::STATUS_* + * @template H of array<string, mixed> + * @template-extends Response<Http::STATUS_*, array<string, mixed>> + */ +class TemplateResponse extends Response { + /** + * @since 20.0.0 + */ + public const RENDER_AS_GUEST = 'guest'; + /** + * @since 20.0.0 + */ + public const RENDER_AS_BLANK = ''; + /** + * @since 20.0.0 + */ + public const RENDER_AS_BASE = 'base'; + /** + * @since 20.0.0 + */ + public const RENDER_AS_USER = 'user'; + /** + * @since 20.0.0 + */ + public const RENDER_AS_ERROR = 'error'; + /** + * @since 20.0.0 + */ + public const RENDER_AS_PUBLIC = 'public'; + + /** + * name of the template + * @var string + */ + protected $templateName; + + /** + * parameters + * @var array + */ + protected $params; + + /** + * rendering type (admin, user, blank) + * @var string + */ + protected $renderAs; + + /** + * app name + * @var string + */ + protected $appName; + + /** + * constructor of TemplateResponse + * @param string $appName the name of the app to load the template from + * @param string $templateName the name of the template + * @param array $params an array of parameters which should be passed to the + * template + * @param string $renderAs how the page should be rendered, defaults to user + * @param S $status + * @param H $headers + * @since 6.0.0 - parameters $params and $renderAs were added in 7.0.0 + */ + public function __construct(string $appName, string $templateName, array $params = [], string $renderAs = self::RENDER_AS_USER, int $status = Http::STATUS_OK, array $headers = []) { + parent::__construct($status, $headers); + + $this->templateName = $templateName; + $this->appName = $appName; + $this->params = $params; + $this->renderAs = $renderAs; + + $this->setContentSecurityPolicy(new ContentSecurityPolicy()); + $this->setFeaturePolicy(new FeaturePolicy()); + } + + + /** + * Sets template parameters + * @param array $params an array with key => value structure which sets template + * variables + * @return TemplateResponse Reference to this object + * @since 6.0.0 - return value was added in 7.0.0 + */ + public function setParams(array $params) { + $this->params = $params; + + return $this; + } + + + /** + * Used for accessing the set parameters + * @return array the params + * @since 6.0.0 + */ + public function getParams() { + return $this->params; + } + + + /** + * @return string the app id of the used template + * @since 25.0.0 + */ + public function getApp(): string { + return $this->appName; + } + + + /** + * Used for accessing the name of the set template + * @return string the name of the used template + * @since 6.0.0 + */ + public function getTemplateName() { + return $this->templateName; + } + + + /** + * Sets the template page + * @param string $renderAs admin, user or blank. Admin also prints the admin + * settings header and footer, user renders the normal + * normal page including footer and header and blank + * just renders the plain template + * @return TemplateResponse Reference to this object + * @since 6.0.0 - return value was added in 7.0.0 + */ + public function renderAs($renderAs) { + $this->renderAs = $renderAs; + + return $this; + } + + + /** + * Returns the set renderAs + * @return string the renderAs value + * @since 6.0.0 + */ + public function getRenderAs() { + return $this->renderAs; + } + + + /** + * Returns the rendered html + * @return string the rendered html + * @since 6.0.0 + */ + public function render() { + $renderAs = self::RENDER_AS_USER; + if ($this->renderAs === 'blank') { + // Legacy fallback as \OCP\Template needs an empty string instead of 'blank' for an unwrapped response + $renderAs = self::RENDER_AS_BLANK; + } elseif (in_array($this->renderAs, [ + self::RENDER_AS_GUEST, + self::RENDER_AS_BLANK, + self::RENDER_AS_BASE, + self::RENDER_AS_ERROR, + self::RENDER_AS_PUBLIC, + self::RENDER_AS_USER], true)) { + $renderAs = $this->renderAs; + } + + $template = Server::get(ITemplateManager::class)->getTemplate($this->appName, $this->templateName, $renderAs); + + foreach ($this->params as $key => $value) { + $template->assign($key, $value); + } + + return $template->fetchPage($this->params); + } +} diff --git a/lib/public/AppFramework/Http/TextPlainResponse.php b/lib/public/AppFramework/Http/TextPlainResponse.php new file mode 100644 index 00000000000..9dfa2c5544d --- /dev/null +++ b/lib/public/AppFramework/Http/TextPlainResponse.php @@ -0,0 +1,47 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Http; + +use OCP\AppFramework\Http; + +/** + * A renderer for text responses + * @since 22.0.0 + * @template S of Http::STATUS_* + * @template H of array<string, mixed> + * @template-extends Response<Http::STATUS_*, array<string, mixed>> + */ +class TextPlainResponse extends Response { + /** @var string */ + private $text = ''; + + /** + * constructor of TextPlainResponse + * @param string $text The text body + * @param S $statusCode the Http status code, defaults to 200 + * @param H $headers + * @since 22.0.0 + */ + public function __construct(string $text = '', int $statusCode = Http::STATUS_OK, array $headers = []) { + parent::__construct($statusCode, $headers); + + $this->text = $text; + $this->addHeader('Content-Type', 'text/plain'); + } + + + /** + * Returns the text + * @return string + * @since 22.0.0 + * @throws \Exception If data could not get encoded + */ + public function render() : string { + return $this->text; + } +} diff --git a/lib/public/AppFramework/Http/TooManyRequestsResponse.php b/lib/public/AppFramework/Http/TooManyRequestsResponse.php new file mode 100644 index 00000000000..f7084ec768d --- /dev/null +++ b/lib/public/AppFramework/Http/TooManyRequestsResponse.php @@ -0,0 +1,41 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Http; + +use OCP\AppFramework\Http; +use OCP\Server; +use OCP\Template\ITemplateManager; + +/** + * A generic 429 response showing an 404 error page as well to the end-user + * @since 19.0.0 + * @template S of Http::STATUS_* + * @template H of array<string, mixed> + * @template-extends Response<Http::STATUS_*, array<string, mixed>> + */ +class TooManyRequestsResponse extends Response { + /** + * @param S $status + * @param H $headers + * @since 19.0.0 + */ + public function __construct(int $status = Http::STATUS_TOO_MANY_REQUESTS, array $headers = []) { + parent::__construct($status, $headers); + + $this->setContentSecurityPolicy(new ContentSecurityPolicy()); + } + + /** + * @return string + * @since 19.0.0 + */ + public function render() { + $template = Server::get(ITemplateManager::class)->getTemplate('core', '429', TemplateResponse::RENDER_AS_BLANK); + return $template->fetchPage(); + } +} diff --git a/lib/public/AppFramework/Http/ZipResponse.php b/lib/public/AppFramework/Http/ZipResponse.php new file mode 100644 index 00000000000..a552eb1294f --- /dev/null +++ b/lib/public/AppFramework/Http/ZipResponse.php @@ -0,0 +1,77 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Http; + +use OC\Streamer; +use OCP\AppFramework\Http; +use OCP\IRequest; + +/** + * Public library to send several files in one zip archive. + * + * @since 15.0.0 + * @template S of Http::STATUS_* + * @template H of array<string, mixed> + * @template-extends Response<Http::STATUS_*, array<string, mixed>> + */ +class ZipResponse extends Response implements ICallbackResponse { + /** @var array{internalName: string, resource: resource, size: int, time: int}[] Files to be added to the zip response */ + private array $resources = []; + /** @var string Filename that the zip file should have */ + private string $name; + private IRequest $request; + + /** + * @param S $status + * @param H $headers + * @since 15.0.0 + */ + public function __construct(IRequest $request, string $name = 'output', int $status = Http::STATUS_OK, array $headers = []) { + parent::__construct($status, $headers); + + $this->name = $name; + $this->request = $request; + } + + /** + * @since 15.0.0 + */ + public function addResource($r, string $internalName, int $size, int $time = -1) { + if (!\is_resource($r)) { + throw new \InvalidArgumentException('No resource provided'); + } + + $this->resources[] = [ + 'resource' => $r, + 'internalName' => $internalName, + 'size' => $size, + 'time' => $time, + ]; + } + + /** + * @since 15.0.0 + */ + public function callback(IOutput $output) { + $size = 0; + $files = count($this->resources); + + foreach ($this->resources as $resource) { + $size += $resource['size']; + } + + $zip = new Streamer($this->request, $size, $files); + $zip->sendHeaders($this->name); + + foreach ($this->resources as $resource) { + $zip->addFileFromStream($resource['resource'], $resource['internalName'], $resource['size'], $resource['time']); + } + + $zip->finalize(); + } +} diff --git a/lib/public/AppFramework/IAppContainer.php b/lib/public/AppFramework/IAppContainer.php new file mode 100644 index 00000000000..c20b252b0ce --- /dev/null +++ b/lib/public/AppFramework/IAppContainer.php @@ -0,0 +1,56 @@ +<?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 OCP\AppFramework; + +use OCP\IContainer; +use Psr\Container\ContainerInterface; + +/** + * This is a tagging interface for a container that belongs to an app + * + * The interface currently extends IContainer, but this interface is deprecated as of Nextcloud 20, + * thus this interface won't extend it anymore once that was removed. So migrate to the ContainerInterface + * only. + * + * @deprecated 20.0.0 + * @since 6.0.0 + */ +interface IAppContainer extends ContainerInterface, IContainer { + /** + * used to return the appname of the set application + * @return string the name of your application + * @since 6.0.0 + * @deprecated 20.0.0 + */ + public function getAppName(); + + /** + * @return \OCP\IServerContainer + * @since 6.0.0 + * @deprecated 20.0.0 + */ + public function getServer(); + + /** + * @param string $middleWare + * @return boolean + * @since 6.0.0 + * @deprecated 20.0.0 use \OCP\AppFramework\Bootstrap\IRegistrationContext::registerMiddleware + */ + public function registerMiddleWare($middleWare); + + /** + * Register a capability + * + * @param string $serviceName e.g. 'OCA\Files\Capabilities' + * @since 8.2.0 + * @deprecated 20.0.0 use \OCP\AppFramework\Bootstrap\IRegistrationContext::registerCapability + */ + public function registerCapability($serviceName); +} diff --git a/lib/public/AppFramework/Middleware.php b/lib/public/AppFramework/Middleware.php new file mode 100644 index 00000000000..33bc288780e --- /dev/null +++ b/lib/public/AppFramework/Middleware.php @@ -0,0 +1,85 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCP\AppFramework; + +use Exception; +use OCP\AppFramework\Http\Response; + +/** + * Middleware is used to provide hooks before or after controller methods and + * deal with possible exceptions raised in the controller methods. + * They're modeled after Django's middleware system: + * https://docs.djangoproject.com/en/dev/topics/http/middleware/ + * @since 6.0.0 + */ +abstract class Middleware { + /** + * This is being run in normal order before the controller is being + * called which allows several modifications and checks + * + * @param Controller $controller the controller that is being called + * @param string $methodName the name of the method that will be called on + * the controller + * @return void + * @since 6.0.0 + */ + public function beforeController(Controller $controller, string $methodName) { + } + + + /** + * This is being run when either the beforeController method or the + * controller method itself is throwing an exception. The middleware is + * asked in reverse order to handle the exception and to return a response. + * If the response is null, it is assumed that the exception could not be + * handled and the error will be thrown again + * + * @param Controller $controller the controller that is being called + * @param string $methodName the name of the method that will be called on + * the controller + * @param Exception $exception the thrown exception + * @throws Exception the passed in exception if it can't handle it + * @return Response a Response object in case that the exception was handled + * @since 6.0.0 + */ + public function afterException(Controller $controller, string $methodName, Exception $exception) { + throw $exception; + } + + + /** + * This is being run after a successful controllermethod call and allows + * the manipulation of a Response object. The middleware is run in reverse order + * + * @param Controller $controller the controller that is being called + * @param string $methodName the name of the method that will be called on + * the controller + * @param Response $response the generated response from the controller + * @return Response a Response object + * @since 6.0.0 + */ + public function afterController(Controller $controller, string $methodName, Response $response) { + return $response; + } + + + /** + * This is being run after the response object has been rendered and + * allows the manipulation of the output. The middleware is run in reverse order + * + * @param Controller $controller the controller that is being called + * @param string $methodName the name of the method that will be called on + * the controller + * @param string $output the generated output from a response + * @return string the output that should be printed + * @since 6.0.0 + */ + public function beforeOutput(Controller $controller, string $methodName, string $output) { + return $output; + } +} diff --git a/lib/public/AppFramework/OCS/OCSBadRequestException.php b/lib/public/AppFramework/OCS/OCSBadRequestException.php new file mode 100644 index 00000000000..77b8ec6c86d --- /dev/null +++ b/lib/public/AppFramework/OCS/OCSBadRequestException.php @@ -0,0 +1,28 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\OCS; + +use Exception; +use OCP\AppFramework\Http; + +/** + * Class OCSBadRequestException + * + * @since 9.1.0 + */ +class OCSBadRequestException extends OCSException { + /** + * OCSBadRequestException constructor. + * + * @param string $message + * @param Exception|null $previous + * @since 9.1.0 + */ + public function __construct($message = '', ?Exception $previous = null) { + parent::__construct($message, Http::STATUS_BAD_REQUEST, $previous); + } +} diff --git a/lib/public/AppFramework/OCS/OCSException.php b/lib/public/AppFramework/OCS/OCSException.php new file mode 100644 index 00000000000..02901992f8d --- /dev/null +++ b/lib/public/AppFramework/OCS/OCSException.php @@ -0,0 +1,17 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\OCS; + +use Exception; + +/** + * Class OCSException + * + * @since 9.1.0 + */ +class OCSException extends Exception { +} diff --git a/lib/public/AppFramework/OCS/OCSForbiddenException.php b/lib/public/AppFramework/OCS/OCSForbiddenException.php new file mode 100644 index 00000000000..0d001377043 --- /dev/null +++ b/lib/public/AppFramework/OCS/OCSForbiddenException.php @@ -0,0 +1,28 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\OCS; + +use Exception; +use OCP\AppFramework\Http; + +/** + * Class OCSForbiddenException + * + * @since 9.1.0 + */ +class OCSForbiddenException extends OCSException { + /** + * OCSForbiddenException constructor. + * + * @param string $message + * @param Exception|null $previous + * @since 9.1.0 + */ + public function __construct($message = '', ?Exception $previous = null) { + parent::__construct($message, Http::STATUS_FORBIDDEN, $previous); + } +} diff --git a/lib/public/AppFramework/OCS/OCSNotFoundException.php b/lib/public/AppFramework/OCS/OCSNotFoundException.php new file mode 100644 index 00000000000..67cea9ed759 --- /dev/null +++ b/lib/public/AppFramework/OCS/OCSNotFoundException.php @@ -0,0 +1,28 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\OCS; + +use Exception; +use OCP\AppFramework\Http; + +/** + * Class OCSNotFoundException + * + * @since 9.1.0 + */ +class OCSNotFoundException extends OCSException { + /** + * OCSNotFoundException constructor. + * + * @param string $message + * @param Exception|null $previous + * @since 9.1.0 + */ + public function __construct($message = '', ?Exception $previous = null) { + parent::__construct($message, Http::STATUS_NOT_FOUND, $previous); + } +} diff --git a/lib/public/AppFramework/OCS/OCSPreconditionFailedException.php b/lib/public/AppFramework/OCS/OCSPreconditionFailedException.php new file mode 100644 index 00000000000..4fc2820eaec --- /dev/null +++ b/lib/public/AppFramework/OCS/OCSPreconditionFailedException.php @@ -0,0 +1,28 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\OCS; + +use Exception; +use OCP\AppFramework\Http; + +/** + * Class OCSPreconditionFailedException + * + * @since 28.0.0 + */ +class OCSPreconditionFailedException extends OCSException { + /** + * OCSPreconditionFailedException constructor. + * + * @param string $message + * @param Exception|null $previous + * @since 9.1.0 + */ + public function __construct($message = '', ?Exception $previous = null) { + parent::__construct($message, Http::STATUS_PRECONDITION_FAILED, $previous); + } +} diff --git a/lib/public/AppFramework/OCSController.php b/lib/public/AppFramework/OCSController.php new file mode 100644 index 00000000000..7cde2a7e427 --- /dev/null +++ b/lib/public/AppFramework/OCSController.php @@ -0,0 +1,107 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCP\AppFramework; + +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\Response; +use OCP\IRequest; + +/** + * Base class to inherit your controllers from that are used for RESTful APIs + * @since 8.1.0 + */ +abstract class OCSController extends ApiController { + /** + * @since 22.0.0 + */ + public const RESPOND_UNAUTHORISED = 997; + + /** + * @since 22.0.0 + */ + public const RESPOND_SERVER_ERROR = 996; + + /** + * @since 22.0.0 + */ + public const RESPOND_NOT_FOUND = 998; + + /** + * @since 22.0.0 + */ + public const RESPOND_UNKNOWN_ERROR = 999; + + /** @var int */ + private $ocsVersion; + + /** + * constructor of the controller + * @param string $appName the name of the app + * @param IRequest $request an instance of the request + * @param string $corsMethods comma separated string of HTTP verbs which + * should be allowed for websites or webapps when calling your API, defaults to + * 'PUT, POST, GET, DELETE, PATCH' + * @param string $corsAllowedHeaders comma separated string of HTTP headers + * which should be allowed for websites or webapps when calling your API, + * defaults to 'Authorization, Content-Type, Accept' + * @param int $corsMaxAge number in seconds how long a preflighted OPTIONS + * request should be cached, defaults to 1728000 seconds + * @since 8.1.0 + */ + public function __construct($appName, + IRequest $request, + $corsMethods = 'PUT, POST, GET, DELETE, PATCH', + $corsAllowedHeaders = 'Authorization, Content-Type, Accept, OCS-APIRequest', + $corsMaxAge = 1728000) { + parent::__construct($appName, $request, $corsMethods, + $corsAllowedHeaders, $corsMaxAge); + $this->registerResponder('json', function ($data) { + return $this->buildOCSResponse('json', $data); + }); + $this->registerResponder('xml', function ($data) { + return $this->buildOCSResponse('xml', $data); + }); + } + + /** + * @param int $version + * @since 11.0.0 + * @internal + */ + public function setOCSVersion($version) { + $this->ocsVersion = $version; + } + + /** + * Since the OCS endpoints default to XML we need to find out the format + * again + * @param mixed $response the value that was returned from a controller and + * is not a Response instance + * @param string $format the format for which a formatter has been registered + * @throws \DomainException if format does not match a registered formatter + * @return Response + * @since 9.1.0 + */ + public function buildResponse($response, $format = 'xml') { + return parent::buildResponse($response, $format); + } + + /** + * Unwrap data and build ocs response + * @param string $format json or xml + * @param DataResponse $data the data which should be transformed + * @since 8.1.0 + * @return \OC\AppFramework\OCS\BaseResponse + */ + private function buildOCSResponse($format, DataResponse $data) { + if ($this->ocsVersion === 1) { + return new \OC\AppFramework\OCS\V1Response($data, $format); + } + return new \OC\AppFramework\OCS\V2Response($data, $format); + } +} diff --git a/lib/public/AppFramework/PublicShareController.php b/lib/public/AppFramework/PublicShareController.php new file mode 100644 index 00000000000..999b3827565 --- /dev/null +++ b/lib/public/AppFramework/PublicShareController.php @@ -0,0 +1,119 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework; + +use OCP\IRequest; +use OCP\ISession; + +/** + * Base controller for public shares + * + * It will verify if the user is properly authenticated to the share. If not a 404 + * is thrown by the PublicShareMiddleware. + * + * Use this for example for a controller that is not to be called via a webbrowser + * directly. For example a PublicPreviewController. As this is not meant to be + * called by a user directly. + * + * To show an auth page extend the AuthPublicShareController + * + * @since 14.0.0 + */ +abstract class PublicShareController extends Controller { + /** @var ISession */ + protected $session; + + /** @var string */ + private $token; + + /** + * @since 14.0.0 + */ + public function __construct(string $appName, + IRequest $request, + ISession $session) { + parent::__construct($appName, $request); + + $this->session = $session; + } + + /** + * Middleware set the token for the request + * + * @since 14.0.0 + */ + final public function setToken(string $token) { + $this->token = $token; + } + + /** + * Get the token for this request + * + * @since 14.0.0 + */ + final public function getToken(): string { + return $this->token; + } + + /** + * Get a hash of the password for this share + * + * To ensure access is blocked when the password to a share is changed we store + * a hash of the password for this token. + * + * @since 14.0.0 + */ + abstract protected function getPasswordHash(): ?string; + + /** + * Is the provided token a valid token + * + * This function is already called from the middleware directly after setting the token. + * + * @since 14.0.0 + */ + abstract public function isValidToken(): bool; + + /** + * Is a share with this token password protected + * + * @since 14.0.0 + */ + abstract protected function isPasswordProtected(): bool; + + /** + * Check if a share is authenticated or not + * + * @since 14.0.0 + */ + public function isAuthenticated(): bool { + // Always authenticated against non password protected shares + if (!$this->isPasswordProtected()) { + return true; + } + + // If we are authenticated properly + if ($this->session->get('public_link_authenticated_token') === $this->getToken() + && $this->session->get('public_link_authenticated_password_hash') === $this->getPasswordHash()) { + return true; + } + + // Fail by default if nothing matches + return false; + } + + /** + * Function called if the share is not found. + * + * You can use this to do some logging for example + * + * @since 14.0.0 + */ + public function shareNotFound() { + } +} diff --git a/lib/public/AppFramework/QueryException.php b/lib/public/AppFramework/QueryException.php new file mode 100644 index 00000000000..20a964e82c8 --- /dev/null +++ b/lib/public/AppFramework/QueryException.php @@ -0,0 +1,23 @@ +<?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 OCP\AppFramework; + +use Exception; +use Psr\Container\ContainerExceptionInterface; + +/** + * Class QueryException + * + * The class extends `ContainerExceptionInterface` since 20.0.0 + * + * @since 8.1.0 + * @deprecated 20.0.0 catch \Psr\Container\ContainerExceptionInterface + */ +class QueryException extends Exception implements ContainerExceptionInterface { +} diff --git a/lib/public/AppFramework/Services/IAppConfig.php b/lib/public/AppFramework/Services/IAppConfig.php new file mode 100644 index 00000000000..aa95e665604 --- /dev/null +++ b/lib/public/AppFramework/Services/IAppConfig.php @@ -0,0 +1,324 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Services; + +use OCP\Exceptions\AppConfigUnknownKeyException; + +/** + * Wrapper for AppConfig for the AppFramework + * + * @since 20.0.0 + */ +interface IAppConfig { + /** + * Get all keys stored for this app + * + * @return string[] the keys stored for the app + * @since 20.0.0 + */ + public function getAppKeys(): array; + + /** + * Check if a key exists in the list of stored config values. + * + * @param string $key config key + * @param bool $lazy search within lazy loaded config + * + * @return bool TRUE if key exists + * @since 29.0.0 + */ + public function hasAppKey(string $key, ?bool $lazy = false): bool; + + /** + * best way to see if a value is set as sensitive (not displayed in report) + * + * @param string $key config key + * @param bool|null $lazy search within lazy loaded config + * + * @return bool TRUE if value is sensitive + * @throws AppConfigUnknownKeyException if config key is not known + * @since 29.0.0 + */ + public function isSensitive(string $key, ?bool $lazy = false): bool; + + /** + * Returns if the config key stored in database is lazy loaded + * + * **WARNING:** ignore lazy filtering, all config values are loaded from database + * + * @param string $key config key + * + * @return bool TRUE if config is lazy loaded + * @throws AppConfigUnknownKeyException if config key is not known + * @see IAppConfig for details about lazy loading + * @since 29.0.0 + */ + public function isLazy(string $key): bool; + + /** + * List all config values from an app with config key starting with $key. + * Returns an array with config key as key, stored value as value. + * + * **WARNING:** ignore lazy filtering, all config values are loaded from database + * + * @param string $key config keys prefix to search, can be empty. + * @param bool $filtered filter sensitive config values + * + * @return array<string, string|int|float|bool|array> [configKey => configValue] + * @since 29.0.0 + */ + public function getAllAppValues(string $key = '', bool $filtered = false): array; + + /** + * Writes a new app wide value + * + * @param string $key the key of the value, under which will be saved + * @param string $value the value that should be stored + * @return void + * @since 20.0.0 + * @deprecated 29.0.0 use {@see setAppValueString()} + */ + public function setAppValue(string $key, string $value): void; + + /** + * Store a config key and its value in database + * + * If config key is already known with the exact same config value, the database is not updated. + * If config key is not supposed to be read during the boot of the cloud, it is advised to set it as lazy loaded. + * + * If config value was previously stored as sensitive or lazy loaded, status cannot be altered without using {@see deleteKey()} first + * + * @param string $key config key + * @param string $value config value + * @param bool $sensitive if TRUE value will be hidden when listing config values. + * @param bool $lazy set config as lazy loaded + * + * @return bool TRUE if value was different, therefor updated in database + * @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; + + /** + * Store a config key and its value in database + * + * When handling huge value around and/or above 2,147,483,647, a debug log will be generated + * on 64bits system, as php int type reach its limit (and throw an exception) on 32bits when using huge numbers. + * + * When using huge numbers, it is advised to use {@see \OCP\Util::numericToNumber()} and {@see setValueString()} + * + * If config key is already known with the exact same config value, the database is not updated. + * If config key is not supposed to be read during the boot of the cloud, it is advised to set it as lazy loaded. + * + * If config value was previously stored as sensitive or lazy loaded, status cannot be altered without using {@see deleteKey()} first + * + * @param string $key config key + * @param int $value config value + * @param bool $sensitive if TRUE value will be hidden when listing config values. + * @param bool $lazy set config as lazy loaded + * + * @return bool TRUE if value was different, therefor updated in database + * @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; + + /** + * Store a config key and its value in database. + * + * If config key is already known with the exact same config value, the database is not updated. + * If config key is not supposed to be read during the boot of the cloud, it is advised to set it as lazy loaded. + * + * If config value was previously stored as sensitive or lazy loaded, status cannot be altered without using {@see deleteKey()} first + * + * @param string $key config key + * @param float $value config value + * @param bool $sensitive if TRUE value will be hidden when listing config values. + * @param bool $lazy set config as lazy loaded + * + * @return bool TRUE if value was different, therefor updated in database + * @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; + + /** + * Store a config key and its value in database + * + * If config key is already known with the exact same config value, the database is not updated. + * If config key is not supposed to be read during the boot of the cloud, it is advised to set it as lazy loaded. + * + * If config value was previously stored as lazy loaded, status cannot be altered without using {@see deleteKey()} first + * + * @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 + * @since 29.0.0 + * @see \OCP\IAppConfig for explanation about lazy loading + */ + public function setAppValueBool(string $key, bool $value, bool $lazy = false): bool; + + /** + * Store a config key and its value in database + * + * If config key is already known with the exact same config value, the database is not updated. + * If config key is not supposed to be read during the boot of the cloud, it is advised to set it as lazy loaded. + * + * If config value was previously stored as sensitive or lazy loaded, status cannot be altered without using {@see deleteKey()} first + * + * @param string $key config key + * @param array $value config value + * @param bool $sensitive if TRUE value will be hidden when listing config values. + * @param bool $lazy set config as lazy loaded + * + * @return bool TRUE if value was different, therefor updated in database + * @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; + + /** + * Looks up an app wide defined value + * + * @param string $key the key of the value, under which it was saved + * @param string $default the default value to be returned if the value isn't set + * + * @return string the saved value + * @since 20.0.0 + * @deprecated 29.0.0 use {@see getAppValueString()} + */ + public function getAppValue(string $key, string $default = ''): string; + + /** + * Get config value assigned to a config key. + * If config key is not found in database, default value is returned. + * If config key is set as lazy loaded, the $lazy argument needs to be set to TRUE. + * + * @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 + * @since 29.0.0 + * @see \OCP\IAppConfig for explanation about lazy loading + */ + public function getAppValueString(string $key, string $default = '', bool $lazy = false): string; + + /** + * Get config value assigned to a config key. + * If config key is not found in database, default value is returned. + * If config key is set as lazy loaded, the $lazy argument needs to be set to TRUE. + * + * @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 + * @since 29.0.0 + * @see \OCP\IAppConfig for explanation about lazy loading + */ + public function getAppValueInt(string $key, int $default = 0, bool $lazy = false): int; + + /** + * Get config value assigned to a config key. + * If config key is not found in database, default value is returned. + * If config key is set as lazy loaded, the $lazy argument needs to be set to TRUE. + * + * @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 + * @since 29.0.0 + * @see \OCP\IAppConfig for explanation about lazy loading + */ + public function getAppValueFloat(string $key, float $default = 0, bool $lazy = false): float; + + /** + * Get config value assigned to a config key. + * If config key is not found in database, default value is returned. + * If config key is set as lazy loaded, the $lazy argument needs to be set to TRUE. + * + * @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 + * @since 29.0.0 + * @see \OCP\IAppConfig for explanation about lazy loading + */ + public function getAppValueBool(string $key, bool $default = false, bool $lazy = false): bool; + + /** + * Get config value assigned to a config key. + * If config key is not found in database, default value is returned. + * If config key is set as lazy loaded, the $lazy argument needs to be set to TRUE. + * + * @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 + * @since 29.0.0 + * @see \OCP\IAppConfig for explanation about lazy loading + */ + public function getAppValueArray(string $key, array $default = [], bool $lazy = false): array; + + /** + * Delete an app wide defined value + * + * @param string $key the key of the value, under which it was saved + * @return void + * @since 20.0.0 + */ + public function deleteAppValue(string $key): void; + + /** + * Removes all keys in appconfig belonging to the app + * + * @return void + * @since 20.0.0 + */ + public function deleteAppValues(): void; + + /** + * Set a user defined value + * + * @param string $userId the userId of the user that we want to store the value under + * @param string $key the key under which the value is being stored + * @param string $value the value that you want to store + * @param string $preCondition only update if the config value was previously the value passed as $preCondition + * @throws \OCP\PreConditionNotMetException if a precondition is specified and is not met + * @throws \UnexpectedValueException when trying to store an unexpected value + * @since 20.0.0 + */ + public function setUserValue(string $userId, string $key, string $value, ?string $preCondition = null): void; + + /** + * Shortcut for getting a user defined value + * + * @param string $userId the userId of the user that we want to store the value under + * @param string $key the key under which the value is being stored + * @param mixed $default the default value to be returned if the value isn't set + * @return string + * @since 20.0.0 + */ + public function getUserValue(string $userId, string $key, string $default = ''): string; + + /** + * Delete a user value + * + * @param string $userId the userId of the user that we want to store the value under + * @param string $key the key under which the value is being stored + * @since 20.0.0 + */ + public function deleteUserValue(string $userId, string $key): void; +} diff --git a/lib/public/AppFramework/Services/IInitialState.php b/lib/public/AppFramework/Services/IInitialState.php new file mode 100644 index 00000000000..ac58bcad3cc --- /dev/null +++ b/lib/public/AppFramework/Services/IInitialState.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Services; + +use Closure; + +/** + * @since 20.0.0 + */ +interface IInitialState { + /** + * Allows an app to provide its initial state to the template system. + * Use this if you know your initial state sill be used for example if + * you are in the render function of you controller. + * + * @since 20.0.0 + * + * @param string $key + * @param bool|int|float|string|array|\JsonSerializable $data + */ + public function provideInitialState(string $key, $data): void; + + /** + * Allows an app to provide its initial state via a lazy method. + * This will call the closure when the template is being generated. + * Use this if your app is injected into pages. Since then the render method + * is not called explicitly. But we do not want to load the state on webdav + * requests for example. + * + * @since 20.0.0 + * + * @param string $key + * @param Closure $closure returns a primitive or an object that implements JsonSerializable + * @psalm-param Closure():bool|Closure():int|Closure():float|Closure():string|Closure():array|Closure():\JsonSerializable $closure + */ + public function provideLazyInitialState(string $key, Closure $closure): void; +} diff --git a/lib/public/AppFramework/Services/InitialStateProvider.php b/lib/public/AppFramework/Services/InitialStateProvider.php new file mode 100644 index 00000000000..d1607bc2262 --- /dev/null +++ b/lib/public/AppFramework/Services/InitialStateProvider.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Services; + +/** + * @since 21.0.0 + */ +abstract class InitialStateProvider implements \JsonSerializable { + /** + * @since 21.0.0 + */ + abstract public function getKey(): string; + + /** + * @since 21.0.0 + */ + abstract public function getData(); + + /** + * @since 21.0.0 + * @return mixed + */ + #[\ReturnTypeWillChange] + final public function jsonSerialize() { + return $this->getData(); + } +} diff --git a/lib/public/AppFramework/Utility/IControllerMethodReflector.php b/lib/public/AppFramework/Utility/IControllerMethodReflector.php new file mode 100644 index 00000000000..95d7fbebb56 --- /dev/null +++ b/lib/public/AppFramework/Utility/IControllerMethodReflector.php @@ -0,0 +1,59 @@ +<?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 OCP\AppFramework\Utility; + +/** + * Interface ControllerMethodReflector + * + * Reads and parses annotations from doc comments + * + * @since 8.0.0 + * @deprecated 22.0.0 will be obsolete with native attributes in PHP8 + * @see https://help.nextcloud.com/t/how-should-we-use-php8-attributes/104278 + */ +interface IControllerMethodReflector { + /** + * @param object $object an object or classname + * @param string $method the method which we want to inspect + * @return void + * @since 8.0.0 + * @deprecated 17.0.0 Reflect should not be called multiple times and only be used internally. This will be removed in Nextcloud 18 + */ + public function reflect($object, string $method); + + /** + * Inspects the PHPDoc parameters for types + * + * @param string $parameter the parameter whose type comments should be + * parsed + * @return string|null type in the type parameters (@param int $something) + * would return int or null if not existing + * @since 8.0.0 + * @deprecated 22.0.0 this method is only used internally + */ + public function getType(string $parameter); + + /** + * @return array the arguments of the method with key => default value + * @since 8.0.0 + * @deprecated 22.0.0 this method is only used internally + */ + public function getParameters(): array; + + /** + * Check if a method contains an annotation + * + * @param string $name the name of the annotation + * @return bool true if the annotation is found + * @since 8.0.0 + * @deprecated 22.0.0 will be obsolete with native attributes in PHP8 + * @see https://help.nextcloud.com/t/how-should-we-use-php8-attributes/104278 + */ + public function hasAnnotation(string $name): bool; +} diff --git a/lib/public/AppFramework/Utility/ITimeFactory.php b/lib/public/AppFramework/Utility/ITimeFactory.php new file mode 100644 index 00000000000..cd63b94dee3 --- /dev/null +++ b/lib/public/AppFramework/Utility/ITimeFactory.php @@ -0,0 +1,51 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCP\AppFramework\Utility; + +use Psr\Clock\ClockInterface; + +/** + * Use this to get a timestamp or DateTime object in code to remain testable + * + * @since 8.0.0 + * @since 27.0.0 Extends the \Psr\Clock\ClockInterface interface + * @ref https://www.php-fig.org/psr/psr-20/#21-clockinterface + */ + +interface ITimeFactory extends ClockInterface { + /** + * @return int the result of a call to time() + * @since 8.0.0 + */ + public function getTime(): int; + + /** + * @param string $time + * @param \DateTimeZone|null $timezone + * @return \DateTime + * @since 15.0.0 + */ + public function getDateTime(string $time = 'now', ?\DateTimeZone $timezone = null): \DateTime; + + /** + * @param \DateTimeZone $timezone + * @return static + * @since 26.0.0 + */ + public function withTimeZone(\DateTimeZone $timezone): static; + + /** + * @param string|null $timezone + * @return \DateTimeZone Requested timezone if provided, UTC otherwise + * @throws \Exception + * @since 29.0.0 + */ + public function getTimeZone(?string $timezone = null): \DateTimeZone; +} |