diff options
Diffstat (limited to 'lib/public/AppFramework/Http')
54 files changed, 3721 insertions, 0 deletions
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(); + } +} |