diff options
Diffstat (limited to 'core/Controller')
42 files changed, 1634 insertions, 1497 deletions
diff --git a/core/Controller/AppPasswordController.php b/core/Controller/AppPasswordController.php index 11aca8ef329..e5edc165bf5 100644 --- a/core/Controller/AppPasswordController.php +++ b/core/Controller/AppPasswordController.php @@ -3,28 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; @@ -34,9 +14,13 @@ use OC\Authentication\Token\IToken; use OC\User\Session; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\BruteForceProtection; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; use OCP\AppFramework\Http\Attribute\UseSession; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSForbiddenException; +use OCP\AppFramework\OCSController; use OCP\Authentication\Exceptions\CredentialsUnavailableException; use OCP\Authentication\Exceptions\InvalidTokenException; use OCP\Authentication\Exceptions\PasswordUnavailableException; @@ -48,7 +32,7 @@ use OCP\IUserManager; use OCP\Security\Bruteforce\IThrottler; use OCP\Security\ISecureRandom; -class AppPasswordController extends \OCP\AppFramework\OCSController { +class AppPasswordController extends OCSController { public function __construct( string $appName, IRequest $request, @@ -65,9 +49,6 @@ class AppPasswordController extends \OCP\AppFramework\OCSController { } /** - * @NoAdminRequired - * @PasswordConfirmationRequired - * * Create app password * * @return DataResponse<Http::STATUS_OK, array{apppassword: string}, array{}> @@ -75,6 +56,8 @@ class AppPasswordController extends \OCP\AppFramework\OCSController { * * 200: App password returned */ + #[NoAdminRequired] + #[PasswordConfirmationRequired] #[ApiRoute(verb: 'GET', url: '/getapppassword', root: '/core')] public function getAppPassword(): DataResponse { // We do not allow the creation of new tokens if this is an app password @@ -94,9 +77,9 @@ class AppPasswordController extends \OCP\AppFramework\OCSController { $password = null; } - $userAgent = $this->request->getHeader('USER_AGENT'); + $userAgent = $this->request->getHeader('user-agent'); - $token = $this->random->generate(72, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS); + $token = $this->random->generate(72, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS); $generatedToken = $this->tokenProvider->generateToken( $token, @@ -118,15 +101,14 @@ class AppPasswordController extends \OCP\AppFramework\OCSController { } /** - * @NoAdminRequired - * * Delete app password * - * @return DataResponse<Http::STATUS_OK, array<empty>, array{}> + * @return DataResponse<Http::STATUS_OK, list<empty>, array{}> * @throws OCSForbiddenException Deleting app password is not allowed * * 200: App password deleted successfully */ + #[NoAdminRequired] #[ApiRoute(verb: 'DELETE', url: '/apppassword', root: '/core')] public function deleteAppPassword(): DataResponse { if (!$this->session->exists('app_password')) { @@ -146,8 +128,6 @@ class AppPasswordController extends \OCP\AppFramework\OCSController { } /** - * @NoAdminRequired - * * Rotate app password * * @return DataResponse<Http::STATUS_OK, array{apppassword: string}, array{}> @@ -155,6 +135,7 @@ class AppPasswordController extends \OCP\AppFramework\OCSController { * * 200: App password returned */ + #[NoAdminRequired] #[ApiRoute(verb: 'POST', url: '/apppassword/rotate', root: '/core')] public function rotateAppPassword(): DataResponse { if (!$this->session->exists('app_password')) { @@ -169,7 +150,7 @@ class AppPasswordController extends \OCP\AppFramework\OCSController { throw new OCSForbiddenException('could not rotate apptoken'); } - $newToken = $this->random->generate(72, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS); + $newToken = $this->random->generate(72, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS); $this->tokenProvider->rotate($token, $appPassword, $newToken); return new DataResponse([ @@ -180,16 +161,15 @@ class AppPasswordController extends \OCP\AppFramework\OCSController { /** * Confirm the user password * - * @NoAdminRequired - * @BruteForceProtection(action=sudo) - * * @param string $password The password of the user * - * @return DataResponse<Http::STATUS_OK, array{lastLogin: int}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array<empty>, array{}> + * @return DataResponse<Http::STATUS_OK, array{lastLogin: int}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, list<empty>, array{}> * * 200: Password confirmation succeeded * 403: Password confirmation failed */ + #[NoAdminRequired] + #[BruteForceProtection(action: 'sudo')] #[UseSession] #[ApiRoute(verb: 'PUT', url: '/apppassword/confirm', root: '/core')] public function confirmUserPassword(string $password): DataResponse { diff --git a/core/Controller/AutoCompleteController.php b/core/Controller/AutoCompleteController.php index 20170546ce5..692fe1b7297 100644 --- a/core/Controller/AutoCompleteController.php +++ b/core/Controller/AutoCompleteController.php @@ -3,36 +3,15 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; -use OCA\Core\ResponseDefinitions; +use OC\Core\ResponseDefinitions; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; use OCP\Collaboration\AutoComplete\AutoCompleteEvent; @@ -58,21 +37,20 @@ class AutoCompleteController extends OCSController { } /** - * @NoAdminRequired - * * Autocomplete a query * * @param string $search Text to search for * @param string|null $itemType Type of the items to search for * @param string|null $itemId ID of the items to search for * @param string|null $sorter can be piped, top prio first, e.g.: "commenters|share-recipients" - * @param int[] $shareTypes Types of shares to search for + * @param list<int> $shareTypes Types of shares to search for * @param int $limit Maximum number of results to return * - * @return DataResponse<Http::STATUS_OK, CoreAutocompleteResult[], array{}> + * @return DataResponse<Http::STATUS_OK, list<CoreAutocompleteResult>, array{}> * * 200: Autocomplete results returned */ + #[NoAdminRequired] #[ApiRoute(verb: 'GET', url: '/autocomplete/get', root: '/core')] public function get(string $search, ?string $itemType, ?string $itemId, ?string $sorter = null, array $shareTypes = [IShare::TYPE_USER], int $limit = 10): DataResponse { // if enumeration/user listings are disabled, we'll receive an empty @@ -122,7 +100,7 @@ class AutoCompleteController extends OCSController { } /** - * @return CoreAutocompleteResult[] + * @return list<CoreAutocompleteResult> */ protected function prepareResultArray(array $results): array { $output = []; @@ -145,7 +123,7 @@ class AutoCompleteController extends OCSController { $shareWithDisplayNameUnique = array_key_exists('shareWithDisplayNameUnique', $result) ? $result['shareWithDisplayNameUnique'] : null; $output[] = [ - 'id' => (string) $result['value']['shareWith'], + 'id' => (string)$result['value']['shareWith'], 'label' => $label, 'icon' => $icon ?? '', 'source' => $type, diff --git a/core/Controller/AvatarController.php b/core/Controller/AvatarController.php index 03f59fd6439..b577b2fd460 100644 --- a/core/Controller/AvatarController.php +++ b/core/Controller/AvatarController.php @@ -1,48 +1,32 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Julien Veyssier <eneiluj@posteo.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Controller; use OC\AppFramework\Utility\TimeFactory; +use OC\NotSquareException; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\DataDisplayResponse; use OCP\AppFramework\Http\FileDisplayResponse; use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http\Response; use OCP\Files\File; use OCP\Files\IRootFolder; +use OCP\Files\NotPermittedException; use OCP\IAvatarManager; use OCP\ICache; use OCP\IL10N; +use OCP\Image; use OCP\IRequest; use OCP\IUserManager; use Psr\Log\LoggerInterface; @@ -64,27 +48,30 @@ class AvatarController extends Controller { protected LoggerInterface $logger, protected ?string $userId, protected TimeFactory $timeFactory, + protected GuestAvatarController $guestAvatarController, ) { parent::__construct($appName, $request); } /** - * @NoAdminRequired - * @NoCSRFRequired * @NoSameSiteCookieRequired - * @PublicPage * * Get the dark avatar * * @param string $userId ID of the user - * @param int $size Size of the avatar - * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string, X-NC-IsCustomAvatar: int}>|JSONResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}> + * @param 64|512 $size Size of the avatar + * @param bool $guestFallback Fallback to guest avatar if not found + * @return FileDisplayResponse<Http::STATUS_OK|Http::STATUS_CREATED, array{Content-Type: string, X-NC-IsCustomAvatar: int}>|JSONResponse<Http::STATUS_NOT_FOUND, list<empty>, array{}>|Response<Http::STATUS_INTERNAL_SERVER_ERROR, array{}> * * 200: Avatar returned + * 201: Avatar returned * 404: Avatar not found */ + #[NoCSRFRequired] + #[PublicPage] #[FrontpageRoute(verb: 'GET', url: '/avatar/{userId}/{size}/dark')] - public function getAvatarDark(string $userId, int $size) { + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] + public function getAvatarDark(string $userId, int $size, bool $guestFallback = false) { if ($size <= 64) { if ($size !== 64) { $this->logger->debug('Avatar requested in deprecated size ' . $size); @@ -106,6 +93,9 @@ class AvatarController extends Controller { ['Content-Type' => $avatarFile->getMimeType(), 'X-NC-IsCustomAvatar' => (int)$avatar->isCustomAvatar()] ); } catch (\Exception $e) { + if ($guestFallback) { + return $this->guestAvatarController->getAvatarDark($userId, $size); + } return new JSONResponse([], Http::STATUS_NOT_FOUND); } @@ -116,22 +106,24 @@ class AvatarController extends Controller { /** - * @NoAdminRequired - * @NoCSRFRequired * @NoSameSiteCookieRequired - * @PublicPage * * Get the avatar * * @param string $userId ID of the user - * @param int $size Size of the avatar - * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string, X-NC-IsCustomAvatar: int}>|JSONResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}> + * @param 64|512 $size Size of the avatar + * @param bool $guestFallback Fallback to guest avatar if not found + * @return FileDisplayResponse<Http::STATUS_OK|Http::STATUS_CREATED, array{Content-Type: string, X-NC-IsCustomAvatar: int}>|JSONResponse<Http::STATUS_NOT_FOUND, list<empty>, array{}>|Response<Http::STATUS_INTERNAL_SERVER_ERROR, array{}> * * 200: Avatar returned + * 201: Avatar returned * 404: Avatar not found */ + #[NoCSRFRequired] + #[PublicPage] #[FrontpageRoute(verb: 'GET', url: '/avatar/{userId}/{size}')] - public function getAvatar(string $userId, int $size) { + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] + public function getAvatar(string $userId, int $size, bool $guestFallback = false) { if ($size <= 64) { if ($size !== 64) { $this->logger->debug('Avatar requested in deprecated size ' . $size); @@ -153,6 +145,9 @@ class AvatarController extends Controller { ['Content-Type' => $avatarFile->getMimeType(), 'X-NC-IsCustomAvatar' => (int)$avatar->isCustomAvatar()] ); } catch (\Exception $e) { + if ($guestFallback) { + return $this->guestAvatarController->getAvatar($userId, $size); + } return new JSONResponse([], Http::STATUS_NOT_FOUND); } @@ -161,9 +156,7 @@ class AvatarController extends Controller { return $response; } - /** - * @NoAdminRequired - */ + #[NoAdminRequired] #[FrontpageRoute(verb: 'POST', url: '/avatar/')] public function postAvatar(?string $path = null): JSONResponse { $files = $this->request->getUploadedFile('files'); @@ -192,7 +185,7 @@ class AvatarController extends Controller { try { $content = $node->getContent(); - } catch (\OCP\Files\NotPermittedException $e) { + } catch (NotPermittedException $e) { return new JSONResponse( ['data' => ['message' => $this->l10n->t('The selected file cannot be read.')]], Http::STATUS_BAD_REQUEST @@ -200,9 +193,8 @@ class AvatarController extends Controller { } } elseif (!is_null($files)) { if ( - $files['error'][0] === 0 && - is_uploaded_file($files['tmp_name'][0]) && - !\OC\Files\Filesystem::isFileBlacklisted($files['tmp_name'][0]) + $files['error'][0] === 0 + && is_uploaded_file($files['tmp_name'][0]) ) { if ($files['size'][0] > 20 * 1024 * 1024) { return new JSONResponse( @@ -240,7 +232,7 @@ class AvatarController extends Controller { } try { - $image = new \OCP\Image(); + $image = new Image(); $image->loadFromData($content); $image->readExif($content); $image->fixOrientation(); @@ -284,9 +276,7 @@ class AvatarController extends Controller { } } - /** - * @NoAdminRequired - */ + #[NoAdminRequired] #[FrontpageRoute(verb: 'DELETE', url: '/avatar/')] public function deleteAvatar(): JSONResponse { try { @@ -300,21 +290,20 @@ class AvatarController extends Controller { } /** - * @NoAdminRequired - * * @return JSONResponse|DataDisplayResponse */ + #[NoAdminRequired] #[FrontpageRoute(verb: 'GET', url: '/avatar/tmp')] public function getTmpAvatar() { $tmpAvatar = $this->cache->get('tmpAvatar'); if (is_null($tmpAvatar)) { return new JSONResponse(['data' => [ - 'message' => $this->l10n->t("No temporary profile picture available, try again") + 'message' => $this->l10n->t('No temporary profile picture available, try again') ]], Http::STATUS_NOT_FOUND); } - $image = new \OCP\Image(); + $image = new Image(); $image->loadFromData($tmpAvatar); $resp = new DataDisplayResponse( @@ -328,30 +317,28 @@ class AvatarController extends Controller { return $resp; } - /** - * @NoAdminRequired - */ + #[NoAdminRequired] #[FrontpageRoute(verb: 'POST', url: '/avatar/cropped')] public function postCroppedAvatar(?array $crop = null): JSONResponse { if (is_null($crop)) { - return new JSONResponse(['data' => ['message' => $this->l10n->t("No crop data provided")]], + return new JSONResponse(['data' => ['message' => $this->l10n->t('No crop data provided')]], Http::STATUS_BAD_REQUEST); } if (!isset($crop['x'], $crop['y'], $crop['w'], $crop['h'])) { - return new JSONResponse(['data' => ['message' => $this->l10n->t("No valid crop data provided")]], + return new JSONResponse(['data' => ['message' => $this->l10n->t('No valid crop data provided')]], Http::STATUS_BAD_REQUEST); } $tmpAvatar = $this->cache->get('tmpAvatar'); if (is_null($tmpAvatar)) { return new JSONResponse(['data' => [ - 'message' => $this->l10n->t("No temporary profile picture available, try again") + 'message' => $this->l10n->t('No temporary profile picture available, try again') ]], Http::STATUS_BAD_REQUEST); } - $image = new \OCP\Image(); + $image = new Image(); $image->loadFromData($tmpAvatar); $image->crop($crop['x'], $crop['y'], (int)round($crop['w']), (int)round($crop['h'])); try { @@ -360,7 +347,7 @@ class AvatarController extends Controller { // Clean up $this->cache->remove('tmpAvatar'); return new JSONResponse(['status' => 'success']); - } catch (\OC\NotSquareException $e) { + } catch (NotSquareException $e) { return new JSONResponse(['data' => ['message' => $this->l10n->t('Crop is not square')]], Http::STATUS_BAD_REQUEST); } catch (\Exception $e) { diff --git a/core/Controller/CSRFTokenController.php b/core/Controller/CSRFTokenController.php index 13ea0011146..edf7c26e94c 100644 --- a/core/Controller/CSRFTokenController.php +++ b/core/Controller/CSRFTokenController.php @@ -3,27 +3,8 @@ declare(strict_types=1); /** - * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; @@ -31,11 +12,12 @@ use OC\Security\CSRF\CsrfTokenManager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\JSONResponse; use OCP\IRequest; -#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] class CSRFTokenController extends Controller { public function __construct( string $appName, @@ -46,11 +28,19 @@ class CSRFTokenController extends Controller { } /** - * @NoAdminRequired - * @NoCSRFRequired - * @PublicPage + * Returns a new CSRF token. + * + * @return JSONResponse<Http::STATUS_OK, array{token: string}, array{}>|JSONResponse<Http::STATUS_FORBIDDEN, list<empty>, array{}> + * + * 200: CSRF token returned + * 403: Strict cookie check failed + * + * @NoTwoFactorRequired */ + #[PublicPage] + #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: '/csrftoken')] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] public function index(): JSONResponse { if (!$this->request->passesStrictCookieCheck()) { return new JSONResponse([], Http::STATUS_FORBIDDEN); diff --git a/core/Controller/ClientFlowLoginController.php b/core/Controller/ClientFlowLoginController.php index 76079e710e3..4464af890c4 100644 --- a/core/Controller/ClientFlowLoginController.php +++ b/core/Controller/ClientFlowLoginController.php @@ -1,55 +1,36 @@ <?php + /** - * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch> - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Mario Danic <mario@lovelyhq.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author RussellAult <RussellAult@users.noreply.github.com> - * @author Sergej Nikolaev <kinolaev@gmail.com> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; use OC\Authentication\Events\AppPasswordCreatedEvent; use OC\Authentication\Exceptions\PasswordlessTokenException; use OC\Authentication\Token\IProvider; -use OC\Authentication\Token\IToken; use OCA\OAuth2\Db\AccessToken; use OCA\OAuth2\Db\AccessTokenMapper; use OCA\OAuth2\Db\ClientMapper; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\Attribute\UseSession; +use OCP\AppFramework\Http\ContentSecurityPolicy; +use OCP\AppFramework\Http\RedirectResponse; use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\StandaloneTemplateResponse; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Authentication\Exceptions\InvalidTokenException; +use OCP\Authentication\Token\IToken; use OCP\Defaults; use OCP\EventDispatcher\IEventDispatcher; +use OCP\IConfig; use OCP\IL10N; use OCP\IRequest; use OCP\ISession; @@ -79,12 +60,13 @@ class ClientFlowLoginController extends Controller { private ICrypto $crypto, private IEventDispatcher $eventDispatcher, private ITimeFactory $timeFactory, + private IConfig $config, ) { parent::__construct($appName, $request); } private function getClientName(): string { - $userAgent = $this->request->getHeader('USER_AGENT'); + $userAgent = $this->request->getHeader('user-agent'); return $userAgent !== '' ? $userAgent : 'unknown'; } @@ -109,13 +91,11 @@ class ClientFlowLoginController extends Controller { return $response; } - /** - * @PublicPage - * @NoCSRFRequired - */ + #[PublicPage] + #[NoCSRFRequired] #[UseSession] #[FrontpageRoute(verb: 'GET', url: '/login/flow')] - public function showAuthPickerPage(string $clientIdentifier = '', string $user = '', int $direct = 0): StandaloneTemplateResponse { + public function showAuthPickerPage(string $clientIdentifier = '', string $user = '', int $direct = 0, string $providedRedirectUri = ''): StandaloneTemplateResponse { $clientName = $this->getClientName(); $client = null; if ($clientIdentifier !== '') { @@ -130,8 +110,8 @@ class ClientFlowLoginController extends Controller { $this->appName, 'error', [ - 'errors' => - [ + 'errors' + => [ [ 'error' => 'Access Forbidden', 'hint' => 'Invalid request', @@ -144,11 +124,11 @@ class ClientFlowLoginController extends Controller { $stateToken = $this->random->generate( 64, - ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_DIGITS + ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS ); $this->session->set(self::STATE_NAME, $stateToken); - $csp = new Http\ContentSecurityPolicy(); + $csp = new ContentSecurityPolicy(); if ($client) { $csp->addAllowedFormActionDomain($client->getRedirectUri()); } else { @@ -168,6 +148,7 @@ class ClientFlowLoginController extends Controller { 'oauthState' => $this->session->get('oauth.state'), 'user' => $user, 'direct' => $direct, + 'providedRedirectUri' => $providedRedirectUri, ], 'guest' ); @@ -177,15 +158,18 @@ class ClientFlowLoginController extends Controller { } /** - * @NoAdminRequired - * @NoCSRFRequired * @NoSameSiteCookieRequired */ + #[NoAdminRequired] + #[NoCSRFRequired] #[UseSession] #[FrontpageRoute(verb: 'GET', url: '/login/flow/grant')] - public function grantPage(string $stateToken = '', + public function grantPage( + string $stateToken = '', string $clientIdentifier = '', - int $direct = 0): StandaloneTemplateResponse { + int $direct = 0, + string $providedRedirectUri = '', + ): Response { if (!$this->isValidToken($stateToken)) { return $this->stateTokenForbiddenResponse(); } @@ -197,7 +181,7 @@ class ClientFlowLoginController extends Controller { $clientName = $client->getName(); } - $csp = new Http\ContentSecurityPolicy(); + $csp = new ContentSecurityPolicy(); if ($client) { $csp->addAllowedFormActionDomain($client->getRedirectUri()); } else { @@ -221,6 +205,7 @@ class ClientFlowLoginController extends Controller { 'serverHost' => $this->getServerPath(), 'oauthState' => $this->session->get('oauth.state'), 'direct' => $direct, + 'providedRedirectUri' => $providedRedirectUri, ], 'guest' ); @@ -229,15 +214,15 @@ class ClientFlowLoginController extends Controller { return $response; } - /** - * @NoAdminRequired - * - * @return Http\RedirectResponse|Response - */ + #[NoAdminRequired] #[UseSession] + #[PasswordConfirmationRequired(strict: false)] #[FrontpageRoute(verb: 'POST', url: '/login/flow')] - public function generateAppPassword(string $stateToken, - string $clientIdentifier = '') { + public function generateAppPassword( + string $stateToken, + string $clientIdentifier = '', + string $providedRedirectUri = '', + ): Response { if (!$this->isValidToken($stateToken)) { $this->session->remove(self::STATE_NAME); return $this->stateTokenForbiddenResponse(); @@ -274,7 +259,7 @@ class ClientFlowLoginController extends Controller { $clientName = $client->getName(); } - $token = $this->random->generate(72, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS); + $token = $this->random->generate(72, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS); $uid = $this->userSession->getUser()->getUID(); $generatedToken = $this->tokenProvider->generateToken( $token, @@ -287,7 +272,7 @@ class ClientFlowLoginController extends Controller { ); if ($client) { - $code = $this->random->generate(128, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS); + $code = $this->random->generate(128, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS); $accessToken = new AccessToken(); $accessToken->setClientId($client->getId()); $accessToken->setEncryptedToken($this->crypto->encrypt($token, $code)); @@ -296,7 +281,19 @@ class ClientFlowLoginController extends Controller { $accessToken->setCodeCreatedAt($this->timeFactory->now()->getTimestamp()); $this->accessTokenMapper->insert($accessToken); + $enableOcClients = $this->config->getSystemValueBool('oauth2.enable_oc_clients', false); + $redirectUri = $client->getRedirectUri(); + if ($enableOcClients && $redirectUri === 'http://localhost:*') { + // Sanity check untrusted redirect URI provided by the client first + if (!preg_match('/^http:\/\/localhost:[0-9]+$/', $providedRedirectUri)) { + $response = new Response(); + $response->setStatus(Http::STATUS_FORBIDDEN); + return $response; + } + + $redirectUri = $providedRedirectUri; + } if (parse_url($redirectUri, PHP_URL_QUERY)) { $redirectUri .= '&'; @@ -321,12 +318,10 @@ class ClientFlowLoginController extends Controller { new AppPasswordCreatedEvent($generatedToken) ); - return new Http\RedirectResponse($redirectUri); + return new RedirectResponse($redirectUri); } - /** - * @PublicPage - */ + #[PublicPage] #[FrontpageRoute(verb: 'POST', url: '/login/flow/apptoken')] public function apptokenRedirect(string $stateToken, string $user, string $password): Response { if (!$this->isValidToken($stateToken)) { @@ -352,7 +347,7 @@ class ClientFlowLoginController extends Controller { } $redirectUri = 'nc://login/server:' . $this->getServerPath() . '&user:' . urlencode($user) . '&password:' . urlencode($password); - return new Http\RedirectResponse($redirectUri); + return new RedirectResponse($redirectUri); } private function getServerPath(): string { @@ -366,7 +361,7 @@ class ClientFlowLoginController extends Controller { $protocol = $this->request->getServerProtocol(); - if ($protocol !== "https") { + if ($protocol !== 'https') { $xForwardedProto = $this->request->getHeader('X-Forwarded-Proto'); $xForwardedSSL = $this->request->getHeader('X-Forwarded-Ssl'); if ($xForwardedProto === 'https' || $xForwardedSSL === 'on') { @@ -374,6 +369,6 @@ class ClientFlowLoginController extends Controller { } } - return $protocol . "://" . $this->request->getServerHost() . $serverPostfix; + return $protocol . '://' . $this->request->getServerHost() . $serverPostfix; } } diff --git a/core/Controller/ClientFlowLoginV2Controller.php b/core/Controller/ClientFlowLoginV2Controller.php index 19c1f9ce251..8c0c1e8179d 100644 --- a/core/Controller/ClientFlowLoginV2Controller.php +++ b/core/Controller/ClientFlowLoginV2Controller.php @@ -3,38 +3,24 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; use OC\Core\Db\LoginFlowV2; +use OC\Core\Exception\LoginFlowV2ClientForbiddenException; use OC\Core\Exception\LoginFlowV2NotFoundException; +use OC\Core\ResponseDefinitions; use OC\Core\Service\LoginFlowV2Service; -use OCA\Core\ResponseDefinitions; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\Attribute\UseSession; use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\RedirectResponse; @@ -49,6 +35,7 @@ use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserSession; use OCP\Security\ISecureRandom; +use OCP\Server; /** * @psalm-import-type CoreLoginFlowV2Credentials from ResponseDefinitions @@ -57,6 +44,8 @@ use OCP\Security\ISecureRandom; class ClientFlowLoginV2Controller extends Controller { public const TOKEN_NAME = 'client.flow.v2.login.token'; public const STATE_NAME = 'client.flow.v2.state.token'; + // Denotes that the session was created for the login flow and should therefore be ephemeral. + public const EPHEMERAL_NAME = 'client.flow.v2.state.ephemeral'; public function __construct( string $appName, @@ -74,18 +63,18 @@ class ClientFlowLoginV2Controller extends Controller { } /** - * @NoCSRFRequired - * @PublicPage - * * Poll the login flow credentials * * @param string $token Token of the flow - * @return JSONResponse<Http::STATUS_OK, CoreLoginFlowV2Credentials, array{}>|JSONResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}> + * @return JSONResponse<Http::STATUS_OK, CoreLoginFlowV2Credentials, array{}>|JSONResponse<Http::STATUS_NOT_FOUND, list<empty>, array{}> * * 200: Login flow credentials returned * 404: Login flow not found or completed */ + #[NoCSRFRequired] + #[PublicPage] #[FrontpageRoute(verb: 'POST', url: '/login/v2/poll')] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] public function poll(string $token): JSONResponse { try { $creds = $this->loginFlowV2Service->poll($token); @@ -96,14 +85,12 @@ class ClientFlowLoginV2Controller extends Controller { return new JSONResponse($creds->jsonSerialize()); } - /** - * @NoCSRFRequired - * @PublicPage - */ + #[NoCSRFRequired] + #[PublicPage] #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] #[UseSession] #[FrontpageRoute(verb: 'GET', url: '/login/v2/flow/{token}')] - public function landing(string $token, $user = ''): Response { + public function landing(string $token, $user = '', int $direct = 0): Response { if (!$this->loginFlowV2Service->startLoginFlow($token)) { return $this->loginTokenForbiddenResponse(); } @@ -111,27 +98,27 @@ class ClientFlowLoginV2Controller extends Controller { $this->session->set(self::TOKEN_NAME, $token); return new RedirectResponse( - $this->urlGenerator->linkToRouteAbsolute('core.ClientFlowLoginV2.showAuthPickerPage', ['user' => $user]) + $this->urlGenerator->linkToRouteAbsolute('core.ClientFlowLoginV2.showAuthPickerPage', ['user' => $user, 'direct' => $direct]) ); } - /** - * @NoCSRFRequired - * @PublicPage - */ + #[NoCSRFRequired] + #[PublicPage] #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] #[UseSession] #[FrontpageRoute(verb: 'GET', url: '/login/v2/flow')] - public function showAuthPickerPage($user = ''): StandaloneTemplateResponse { + public function showAuthPickerPage(string $user = '', int $direct = 0): StandaloneTemplateResponse { try { $flow = $this->getFlowByLoginToken(); } catch (LoginFlowV2NotFoundException $e) { return $this->loginTokenForbiddenResponse(); + } catch (LoginFlowV2ClientForbiddenException $e) { + return $this->loginTokenForbiddenClientResponse(); } $stateToken = $this->random->generate( 64, - ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_DIGITS + ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS ); $this->session->set(self::STATE_NAME, $stateToken); @@ -144,20 +131,21 @@ class ClientFlowLoginV2Controller extends Controller { 'urlGenerator' => $this->urlGenerator, 'stateToken' => $stateToken, 'user' => $user, + 'direct' => $direct, ], 'guest' ); } /** - * @NoAdminRequired - * @NoCSRFRequired * @NoSameSiteCookieRequired */ + #[NoAdminRequired] + #[NoCSRFRequired] #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] #[UseSession] #[FrontpageRoute(verb: 'GET', url: '/login/v2/grant')] - public function grantPage(?string $stateToken): StandaloneTemplateResponse { + public function grantPage(?string $stateToken, int $direct = 0): StandaloneTemplateResponse { if ($stateToken === null) { return $this->stateTokenMissingResponse(); } @@ -169,6 +157,8 @@ class ClientFlowLoginV2Controller extends Controller { $flow = $this->getFlowByLoginToken(); } catch (LoginFlowV2NotFoundException $e) { return $this->loginTokenForbiddenResponse(); + } catch (LoginFlowV2ClientForbiddenException $e) { + return $this->loginTokenForbiddenClientResponse(); } /** @var IUser $user */ @@ -184,14 +174,13 @@ class ClientFlowLoginV2Controller extends Controller { 'instanceName' => $this->defaults->getName(), 'urlGenerator' => $this->urlGenerator, 'stateToken' => $stateToken, + 'direct' => $direct, ], 'guest' ); } - /** - * @PublicPage - */ + #[PublicPage] #[FrontpageRoute(verb: 'POST', url: '/login/v2/apptoken')] public function apptokenRedirect(?string $stateToken, string $user, string $password) { if ($stateToken === null) { @@ -206,6 +195,8 @@ class ClientFlowLoginV2Controller extends Controller { $this->getFlowByLoginToken(); } catch (LoginFlowV2NotFoundException $e) { return $this->loginTokenForbiddenResponse(); + } catch (LoginFlowV2ClientForbiddenException $e) { + return $this->loginTokenForbiddenClientResponse(); } $loginToken = $this->session->get(self::TOKEN_NAME); @@ -215,7 +206,7 @@ class ClientFlowLoginV2Controller extends Controller { $this->session->remove(self::STATE_NAME); try { - $token = \OC::$server->get(\OC\Authentication\Token\IProvider::class)->getToken($password); + $token = Server::get(\OC\Authentication\Token\IProvider::class)->getToken($password); if ($token->getLoginName() !== $user) { throw new InvalidTokenException('login name does not match'); } @@ -236,10 +227,9 @@ class ClientFlowLoginV2Controller extends Controller { return $this->handleFlowDone($result); } - /** - * @NoAdminRequired - */ + #[NoAdminRequired] #[UseSession] + #[PasswordConfirmationRequired(strict: false)] #[FrontpageRoute(verb: 'POST', url: '/login/v2/grant')] public function generateAppPassword(?string $stateToken): Response { if ($stateToken === null) { @@ -253,6 +243,8 @@ class ClientFlowLoginV2Controller extends Controller { $this->getFlowByLoginToken(); } catch (LoginFlowV2NotFoundException $e) { return $this->loginTokenForbiddenResponse(); + } catch (LoginFlowV2ClientForbiddenException $e) { + return $this->loginTokenForbiddenClientResponse(); } $loginToken = $this->session->get(self::TOKEN_NAME); @@ -289,19 +281,19 @@ class ClientFlowLoginV2Controller extends Controller { } /** - * @NoCSRFRequired - * @PublicPage - * * Init a login flow * * @return JSONResponse<Http::STATUS_OK, CoreLoginFlowV2, array{}> * * 200: Login flow init returned */ + #[NoCSRFRequired] + #[PublicPage] #[FrontpageRoute(verb: 'POST', url: '/login/v2')] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] public function init(): JSONResponse { // Get client user agent - $userAgent = $this->request->getHeader('USER_AGENT'); + $userAgent = $this->request->getHeader('user-agent'); $tokens = $this->loginFlowV2Service->createTokens($userAgent); @@ -353,6 +345,7 @@ class ClientFlowLoginV2Controller extends Controller { /** * @return LoginFlowV2 * @throws LoginFlowV2NotFoundException + * @throws LoginFlowV2ClientForbiddenException */ private function getFlowByLoginToken(): LoginFlowV2 { $currentToken = $this->session->get(self::TOKEN_NAME); @@ -376,6 +369,19 @@ class ClientFlowLoginV2Controller extends Controller { return $response; } + private function loginTokenForbiddenClientResponse(): StandaloneTemplateResponse { + $response = new StandaloneTemplateResponse( + $this->appName, + '403', + [ + 'message' => $this->l10n->t('Please use original client'), + ], + 'guest' + ); + $response->setStatus(Http::STATUS_FORBIDDEN); + return $response; + } + private function getServerPath(): string { $serverPostfix = ''; diff --git a/core/Controller/CollaborationResourcesController.php b/core/Controller/CollaborationResourcesController.php index 9d7d7148468..e160d733176 100644 --- a/core/Controller/CollaborationResourcesController.php +++ b/core/Controller/CollaborationResourcesController.php @@ -3,36 +3,17 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; use Exception; -use OCA\Core\ResponseDefinitions; +use OC\Core\ResponseDefinitions; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; use OCP\Collaboration\Resources\CollectionException; @@ -75,16 +56,15 @@ class CollaborationResourcesController extends OCSController { } /** - * @NoAdminRequired - * * Get a collection * * @param int $collectionId ID of the collection - * @return DataResponse<Http::STATUS_OK, CoreCollection, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array<empty>, array{}> + * @return DataResponse<Http::STATUS_OK, CoreCollection, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, list<empty>, array{}> * * 200: Collection returned * 404: Collection not found */ + #[NoAdminRequired] #[ApiRoute(verb: 'GET', url: '/resources/collections/{collectionId}', root: '/collaboration')] public function listCollection(int $collectionId): DataResponse { try { @@ -97,16 +77,15 @@ class CollaborationResourcesController extends OCSController { } /** - * @NoAdminRequired - * * Search for collections * * @param string $filter Filter collections - * @return DataResponse<Http::STATUS_OK, CoreCollection[], array{}>|DataResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}> + * @return DataResponse<Http::STATUS_OK, list<CoreCollection>, array{}>|DataResponse<Http::STATUS_NOT_FOUND, list<empty>, array{}> * * 200: Collections returned * 404: Collection not found */ + #[NoAdminRequired] #[ApiRoute(verb: 'GET', url: '/resources/collections/search/{filter}', root: '/collaboration')] public function searchCollections(string $filter): DataResponse { try { @@ -119,18 +98,17 @@ class CollaborationResourcesController extends OCSController { } /** - * @NoAdminRequired - * * Add a resource to a collection * * @param int $collectionId ID of the collection * @param string $resourceType Name of the resource * @param string $resourceId ID of the resource - * @return DataResponse<Http::STATUS_OK, CoreCollection, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array<empty>, array{}> + * @return DataResponse<Http::STATUS_OK, CoreCollection, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, list<empty>, array{}> * * 200: Collection returned * 404: Collection not found or resource inaccessible */ + #[NoAdminRequired] #[ApiRoute(verb: 'POST', url: '/resources/collections/{collectionId}', root: '/collaboration')] public function addResource(int $collectionId, string $resourceType, string $resourceId): DataResponse { try { @@ -154,18 +132,17 @@ class CollaborationResourcesController extends OCSController { } /** - * @NoAdminRequired - * * Remove a resource from a collection * * @param int $collectionId ID of the collection * @param string $resourceType Name of the resource * @param string $resourceId ID of the resource - * @return DataResponse<Http::STATUS_OK, CoreCollection, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array<empty>, array{}> + * @return DataResponse<Http::STATUS_OK, CoreCollection, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, list<empty>, array{}> * * 200: Collection returned * 404: Collection or resource not found */ + #[NoAdminRequired] #[ApiRoute(verb: 'DELETE', url: '/resources/collections/{collectionId}', root: '/collaboration')] public function removeResource(int $collectionId, string $resourceType, string $resourceId): DataResponse { try { @@ -186,17 +163,16 @@ class CollaborationResourcesController extends OCSController { } /** - * @NoAdminRequired - * * Get collections by resource * * @param string $resourceType Type of the resource * @param string $resourceId ID of the resource - * @return DataResponse<Http::STATUS_OK, CoreCollection[], array{}>|DataResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}> + * @return DataResponse<Http::STATUS_OK, list<CoreCollection>, array{}>|DataResponse<Http::STATUS_NOT_FOUND, list<empty>, array{}> * * 200: Collections returned * 404: Resource not accessible */ + #[NoAdminRequired] #[ApiRoute(verb: 'GET', url: '/resources/{resourceType}/{resourceId}', root: '/collaboration')] public function getCollectionsByResource(string $resourceType, string $resourceId): DataResponse { try { @@ -213,19 +189,18 @@ class CollaborationResourcesController extends OCSController { } /** - * @NoAdminRequired - * * Create a collection for a resource * * @param string $baseResourceType Type of the base resource * @param string $baseResourceId ID of the base resource * @param string $name Name of the collection - * @return DataResponse<Http::STATUS_OK, CoreCollection, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array<empty>, array{}> + * @return DataResponse<Http::STATUS_OK, CoreCollection, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, list<empty>, array{}> * * 200: Collection returned * 400: Creating collection is not possible * 404: Resource inaccessible */ + #[NoAdminRequired] #[ApiRoute(verb: 'POST', url: '/resources/{baseResourceType}/{baseResourceId}', root: '/collaboration')] public function createCollectionOnResource(string $baseResourceType, string $baseResourceId, string $name): DataResponse { if (!isset($name[0]) || isset($name[64])) { @@ -249,17 +224,16 @@ class CollaborationResourcesController extends OCSController { } /** - * @NoAdminRequired - * * Rename a collection * * @param int $collectionId ID of the collection * @param string $collectionName New name - * @return DataResponse<Http::STATUS_OK, CoreCollection, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array<empty>, array{}> + * @return DataResponse<Http::STATUS_OK, CoreCollection, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, list<empty>, array{}> * * 200: Collection returned * 404: Collection not found */ + #[NoAdminRequired] #[ApiRoute(verb: 'PUT', url: '/resources/collections/{collectionId}', root: '/collaboration')] public function renameCollection(int $collectionId, string $collectionName): DataResponse { try { @@ -274,7 +248,7 @@ class CollaborationResourcesController extends OCSController { } /** - * @return DataResponse<Http::STATUS_OK, CoreCollection, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array<empty>, array{}> + * @return DataResponse<Http::STATUS_OK, CoreCollection, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, list<empty>, array{}> */ protected function respondCollection(ICollection $collection): DataResponse { try { @@ -288,7 +262,7 @@ class CollaborationResourcesController extends OCSController { } /** - * @return CoreCollection[] + * @return list<CoreCollection> */ protected function prepareCollections(array $collections): array { $result = []; @@ -321,7 +295,7 @@ class CollaborationResourcesController extends OCSController { } /** - * @return CoreResource[] + * @return list<CoreResource> */ protected function prepareResources(array $resources): array { $result = []; diff --git a/core/Controller/ContactsMenuController.php b/core/Controller/ContactsMenuController.php index e70349970a3..d90ee8a1c61 100644 --- a/core/Controller/ContactsMenuController.php +++ b/core/Controller/ContactsMenuController.php @@ -1,26 +1,8 @@ <?php + /** - * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; @@ -29,6 +11,7 @@ use OC\Contacts\ContactsMenu\Manager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\JSONResponse; use OCP\IRequest; use OCP\IUserSession; @@ -43,22 +26,20 @@ class ContactsMenuController extends Controller { } /** - * @NoAdminRequired - * * @return \JsonSerializable[] * @throws Exception */ + #[NoAdminRequired] #[FrontpageRoute(verb: 'POST', url: '/contactsmenu/contacts')] public function index(?string $filter = null): array { return $this->manager->getEntries($this->userSession->getUser(), $filter); } /** - * @NoAdminRequired - * * @return JSONResponse|\JsonSerializable * @throws Exception */ + #[NoAdminRequired] #[FrontpageRoute(verb: 'POST', url: '/contactsmenu/findOne')] public function findOne(int $shareType, string $shareWith) { $contact = $this->manager->findOne($this->userSession->getUser(), $shareType, $shareWith); diff --git a/core/Controller/CssController.php b/core/Controller/CssController.php index 3fd0c524b06..37e7edc530f 100644 --- a/core/Controller/CssController.php +++ b/core/Controller/CssController.php @@ -3,31 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016, John Molakvoæ (skjnldsv@protonmail.com) - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; @@ -35,7 +12,9 @@ use OC\Files\AppData\Factory; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\FileDisplayResponse; use OCP\AppFramework\Http\NotFoundResponse; use OCP\AppFramework\Http\Response; @@ -62,14 +41,14 @@ class CssController extends Controller { } /** - * @PublicPage - * @NoCSRFRequired * @NoSameSiteCookieRequired * * @param string $fileName css filename with extension * @param string $appName css folder name * @return FileDisplayResponse|NotFoundResponse */ + #[PublicPage] + #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: '/css/{appName}/{fileName}')] public function getCss(string $fileName, string $appName): Response { try { @@ -86,11 +65,11 @@ class CssController extends Controller { } $ttl = 31536000; - $response->addHeader('Cache-Control', 'max-age='.$ttl.', immutable'); + $response->addHeader('Cache-Control', 'max-age=' . $ttl . ', immutable'); $expires = new \DateTime(); $expires->setTimestamp($this->timeFactory->getTime()); - $expires->add(new \DateInterval('PT'.$ttl.'S')); + $expires->add(new \DateInterval('PT' . $ttl . 'S')); $response->addHeader('Expires', $expires->format(\DateTime::RFC1123)); return $response; } diff --git a/core/Controller/ErrorController.php b/core/Controller/ErrorController.php index 040b75be87b..d80dc3f76eb 100644 --- a/core/Controller/ErrorController.php +++ b/core/Controller/ErrorController.php @@ -3,41 +3,24 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net> - * - * @author Julius Härtl <jus@bitgrid.net> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; +use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\TemplateResponse; #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] -class ErrorController extends \OCP\AppFramework\Controller { - /** - * @PublicPage - * @NoCSRFRequired - */ +class ErrorController extends Controller { + #[PublicPage] + #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: 'error/403')] public function error403(): TemplateResponse { $response = new TemplateResponse( @@ -50,10 +33,8 @@ class ErrorController extends \OCP\AppFramework\Controller { return $response; } - /** - * @PublicPage - * @NoCSRFRequired - */ + #[PublicPage] + #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: 'error/404')] public function error404(): TemplateResponse { $response = new TemplateResponse( diff --git a/core/Controller/GuestAvatarController.php b/core/Controller/GuestAvatarController.php index 5e6f2438dd6..711158e0708 100644 --- a/core/Controller/GuestAvatarController.php +++ b/core/Controller/GuestAvatarController.php @@ -1,31 +1,17 @@ <?php + /** - * @copyright Copyright (c) 2019, Michael Weimann <mail@michael-weimann.eu> - * - * @author Michael Weimann <mail@michael-weimann.eu> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\FileDisplayResponse; use OCP\AppFramework\Http\Response; use OCP\IAvatarManager; @@ -51,20 +37,19 @@ class GuestAvatarController extends Controller { /** * Returns a guest avatar image response * - * @PublicPage - * @NoCSRFRequired - * * @param string $guestName The guest name, e.g. "Albert" - * @param string $size The desired avatar size, e.g. 64 for 64x64px + * @param 64|512 $size The desired avatar size, e.g. 64 for 64x64px * @param bool|null $darkTheme Return dark avatar - * @return FileDisplayResponse<Http::STATUS_OK|Http::STATUS_CREATED, array{Content-Type: string}>|Response<Http::STATUS_INTERNAL_SERVER_ERROR, array{}> + * @return FileDisplayResponse<Http::STATUS_OK|Http::STATUS_CREATED, array{Content-Type: string, X-NC-IsCustomAvatar: int}>|Response<Http::STATUS_INTERNAL_SERVER_ERROR, array{}> * * 200: Custom avatar returned * 201: Avatar returned */ + #[PublicPage] + #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: '/avatar/guest/{guestName}/{size}')] - public function getAvatar(string $guestName, string $size, ?bool $darkTheme = false) { - $size = (int) $size; + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] + public function getAvatar(string $guestName, int $size, ?bool $darkTheme = false) { $darkTheme = $darkTheme ?? false; if ($size <= 64) { @@ -86,13 +71,13 @@ class GuestAvatarController extends Controller { $resp = new FileDisplayResponse( $avatarFile, $avatar->isCustomAvatar() ? Http::STATUS_OK : Http::STATUS_CREATED, - ['Content-Type' => $avatarFile->getMimeType()] + ['Content-Type' => $avatarFile->getMimeType(), 'X-NC-IsCustomAvatar' => (int)$avatar->isCustomAvatar()] ); } catch (\Exception $e) { $this->logger->error('error while creating guest avatar', [ 'err' => $e, ]); - $resp = new Http\Response(); + $resp = new Response(); $resp->setStatus(Http::STATUS_INTERNAL_SERVER_ERROR); return $resp; } @@ -105,18 +90,18 @@ class GuestAvatarController extends Controller { /** * Returns a dark guest avatar image response * - * @PublicPage - * @NoCSRFRequired - * * @param string $guestName The guest name, e.g. "Albert" - * @param string $size The desired avatar size, e.g. 64 for 64x64px - * @return FileDisplayResponse<Http::STATUS_OK|Http::STATUS_CREATED, array{Content-Type: string}>|Response<Http::STATUS_INTERNAL_SERVER_ERROR, array{}> + * @param 64|512 $size The desired avatar size, e.g. 64 for 64x64px + * @return FileDisplayResponse<Http::STATUS_OK|Http::STATUS_CREATED, array{Content-Type: string, X-NC-IsCustomAvatar: int}>|Response<Http::STATUS_INTERNAL_SERVER_ERROR, array{}> * * 200: Custom avatar returned * 201: Avatar returned */ + #[PublicPage] + #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: '/avatar/guest/{guestName}/{size}/dark')] - public function getAvatarDark(string $guestName, string $size) { + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] + public function getAvatarDark(string $guestName, int $size) { return $this->getAvatar($guestName, $size, true); } } diff --git a/core/Controller/HoverCardController.php b/core/Controller/HoverCardController.php index 705d506057a..236a81760ac 100644 --- a/core/Controller/HoverCardController.php +++ b/core/Controller/HoverCardController.php @@ -2,34 +2,18 @@ declare(strict_types=1); /** - * @copyright 2021 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; use OC\Contacts\ContactsMenu\Manager; -use OCA\Core\ResponseDefinitions; +use OC\Core\ResponseDefinitions; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCSController; use OCP\IRequest; use OCP\IUserSession; use OCP\Share\IShare; @@ -37,7 +21,7 @@ use OCP\Share\IShare; /** * @psalm-import-type CoreContactsAction from ResponseDefinitions */ -class HoverCardController extends \OCP\AppFramework\OCSController { +class HoverCardController extends OCSController { public function __construct( IRequest $request, private IUserSession $userSession, @@ -47,16 +31,15 @@ class HoverCardController extends \OCP\AppFramework\OCSController { } /** - * @NoAdminRequired - * * Get the account details for a hovercard * * @param string $userId ID of the user - * @return DataResponse<Http::STATUS_OK, array{userId: string, displayName: string, actions: CoreContactsAction[]}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}> + * @return DataResponse<Http::STATUS_OK, array{userId: string, displayName: string, actions: list<CoreContactsAction>}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, list<empty>, array{}> * * 200: Account details returned * 404: Account not found */ + #[NoAdminRequired] #[ApiRoute(verb: 'GET', url: '/v1/{userId}', root: '/hovercard')] public function getUser(string $userId): DataResponse { $contact = $this->manager->findOne($this->userSession->getUser(), IShare::TYPE_USER, $userId); @@ -72,7 +55,7 @@ class HoverCardController extends \OCP\AppFramework\OCSController { array_unshift($actions, $data['topAction']); } - /** @var CoreContactsAction[] $actions */ + /** @var list<CoreContactsAction> $actions */ return new DataResponse([ 'userId' => $userId, 'displayName' => $contact->getFullName(), diff --git a/core/Controller/JsController.php b/core/Controller/JsController.php index 1f504e05ed0..5754c554e50 100644 --- a/core/Controller/JsController.php +++ b/core/Controller/JsController.php @@ -3,31 +3,8 @@ declare(strict_types=1); /** - * @copyright 2017, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; @@ -35,7 +12,9 @@ use OC\Files\AppData\Factory; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\FileDisplayResponse; use OCP\AppFramework\Http\NotFoundResponse; use OCP\AppFramework\Http\Response; @@ -62,14 +41,14 @@ class JsController extends Controller { } /** - * @PublicPage - * @NoCSRFRequired * @NoSameSiteCookieRequired * * @param string $fileName js filename with extension * @param string $appName js folder name * @return FileDisplayResponse|NotFoundResponse */ + #[PublicPage] + #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: '/js/{appName}/{fileName}')] public function getJs(string $fileName, string $appName): Response { try { @@ -86,11 +65,11 @@ class JsController extends Controller { } $ttl = 31536000; - $response->addHeader('Cache-Control', 'max-age='.$ttl.', immutable'); + $response->addHeader('Cache-Control', 'max-age=' . $ttl . ', immutable'); $expires = new \DateTime(); $expires->setTimestamp($this->timeFactory->getTime()); - $expires->add(new \DateInterval('PT'.$ttl.'S')); + $expires->add(new \DateInterval('PT' . $ttl . 'S')); $response->addHeader('Expires', $expires->format(\DateTime::RFC1123)); return $response; } diff --git a/core/Controller/LoginController.php b/core/Controller/LoginController.php index f22fee4f5e7..5a21d27898f 100644 --- a/core/Controller/LoginController.php +++ b/core/Controller/LoginController.php @@ -3,35 +3,9 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2017, Sandro Lutz <sandro.lutz@temparus.ch> - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Michael Weimann <mail@michael-weimann.eu> - * @author Rayn0r <andrew@ilpss8.myfirewall.org> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Controller; @@ -46,12 +20,16 @@ use OCA\User_LDAP\Helper; use OCP\App\IAppManager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\BruteForceProtection; use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\Attribute\UseSession; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\RedirectResponse; +use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Services\IInitialState; use OCP\Defaults; @@ -64,12 +42,15 @@ use OCP\IUser; use OCP\IUserManager; use OCP\Notification\IManager; use OCP\Security\Bruteforce\IThrottler; +use OCP\Security\ITrustedDomainHelper; +use OCP\Server; use OCP\Util; class LoginController extends Controller { public const LOGIN_MSG_INVALIDPASSWORD = 'invalidpassword'; public const LOGIN_MSG_USERDISABLED = 'userdisabled'; public const LOGIN_MSG_CSRFCHECKFAILED = 'csrfCheckFailed'; + public const LOGIN_MSG_INVALID_ORIGIN = 'invalidOrigin'; public function __construct( ?string $appName, @@ -91,10 +72,9 @@ class LoginController extends Controller { } /** - * @NoAdminRequired - * * @return RedirectResponse */ + #[NoAdminRequired] #[UseSession] #[FrontpageRoute(verb: 'GET', url: '/logout')] public function logout() { @@ -113,8 +93,8 @@ class LoginController extends Controller { $this->session->close(); if ( - $this->request->getServerProtocol() === 'https' && - !$this->request->isUserAgent([Request::USER_AGENT_CHROME, Request::USER_AGENT_ANDROID_MOBILE_CHROME]) + $this->request->getServerProtocol() === 'https' + && !$this->request->isUserAgent([Request::USER_AGENT_CHROME, Request::USER_AGENT_ANDROID_MOBILE_CHROME]) ) { $response->addHeader('Clear-Site-Data', '"cache", "storage"'); } @@ -123,18 +103,17 @@ class LoginController extends Controller { } /** - * @PublicPage - * @NoCSRFRequired - * * @param string $user * @param string $redirect_url * * @return TemplateResponse|RedirectResponse */ + #[NoCSRFRequired] + #[PublicPage] #[UseSession] #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] #[FrontpageRoute(verb: 'GET', url: '/login')] - public function showLoginForm(?string $user = null, ?string $redirect_url = null): Http\Response { + public function showLoginForm(?string $user = null, ?string $redirect_url = null): Response { if ($this->userSession->isLoggedIn()) { return new RedirectResponse($this->urlGenerator->linkToDefaultPageUrl()); } @@ -192,6 +171,9 @@ class LoginController extends Controller { Util::addHeader('meta', ['property' => 'og:type', 'content' => 'website']); Util::addHeader('meta', ['property' => 'og:image', 'content' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('core', 'favicon-touch.png'))]); + // Add same-origin referrer policy so we can check for valid requests + Util::addHeader('meta', ['name' => 'referrer', 'content' => 'same-origin']); + $parameters = [ 'alt_login' => OC_App::getAlternativeLogIns(), 'pageTitle' => $this->l10n->t('Login'), @@ -233,7 +215,7 @@ class LoginController extends Controller { $this->canResetPassword($passwordLink, $user) ); } - + /** * Sets the initial state of whether or not a user is allowed to login with their email * initial state is passed in the array of 1 for email allowed and 0 for not allowed @@ -244,7 +226,7 @@ class LoginController extends Controller { // check if user_ldap is enabled, and the required classes exist if ($this->appManager->isAppLoaded('user_ldap') && class_exists(Helper::class)) { - $helper = \OCP\Server::get(Helper::class); + $helper = Server::get(Helper::class); $allPrefixes = $helper->getServerConfigurationPrefixes(); // check each LDAP server the user is connected too foreach ($allPrefixes as $prefix) { @@ -294,30 +276,42 @@ class LoginController extends Controller { return new RedirectResponse($this->urlGenerator->linkToDefaultPageUrl()); } - /** - * @PublicPage - * @NoCSRFRequired - * @BruteForceProtection(action=login) - * - * @return RedirectResponse - */ + #[NoCSRFRequired] + #[PublicPage] + #[BruteForceProtection(action: 'login')] #[UseSession] #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] #[FrontpageRoute(verb: 'POST', url: '/login')] - public function tryLogin(Chain $loginChain, + public function tryLogin( + Chain $loginChain, + ITrustedDomainHelper $trustedDomainHelper, string $user = '', string $password = '', ?string $redirect_url = null, string $timezone = '', - string $timezone_offset = ''): RedirectResponse { - if (!$this->request->passesCSRFCheck()) { + string $timezone_offset = '', + ): RedirectResponse { + $error = ''; + + $origin = $this->request->getHeader('Origin'); + $throttle = true; + if ($origin === '' || !$trustedDomainHelper->isTrustedUrl($origin)) { + // Login attempt not from the same origin, + // We only allow this on the login flow but not on the UI login page. + // This could have come from someone malicious who tries to block a user by triggering the bruteforce protection. + $error = self::LOGIN_MSG_INVALID_ORIGIN; + $throttle = false; + } elseif (!$this->request->passesCSRFCheck()) { if ($this->userSession->isLoggedIn()) { // If the user is already logged in and the CSRF check does not pass then // simply redirect the user to the correct page as required. This is the // case when a user has already logged-in, in another tab. return $this->generateRedirect($redirect_url); } + $error = self::LOGIN_MSG_CSRFCHECKFAILED; + } + if ($error !== '') { // Clear any auth remnants like cookies to ensure a clean login // For the next attempt $this->userSession->logout(); @@ -325,7 +319,8 @@ class LoginController extends Controller { $user, $user, $redirect_url, - self::LOGIN_MSG_CSRFCHECKFAILED + $error, + $throttle, ); } @@ -375,7 +370,12 @@ class LoginController extends Controller { * @return RedirectResponse */ private function createLoginFailedResponse( - $user, $originalUser, $redirect_url, string $loginMessage) { + $user, + $originalUser, + $redirect_url, + string $loginMessage, + bool $throttle = true, + ) { // Read current user and append if possible we need to // return the unmodified user otherwise we will leak the login name $args = $user !== null ? ['user' => $originalUser, 'direct' => 1] : []; @@ -385,31 +385,34 @@ class LoginController extends Controller { $response = new RedirectResponse( $this->urlGenerator->linkToRoute('core.login.showLoginForm', $args) ); - $response->throttle(['user' => substr($user, 0, 64)]); + if ($throttle) { + $response->throttle(['user' => substr($user, 0, 64)]); + } $this->session->set('loginMessages', [ [$loginMessage], [] ]); + return $response; } /** * Confirm the user password * - * @NoAdminRequired - * @BruteForceProtection(action=sudo) - * * @license GNU AGPL version 3 or any later version * * @param string $password The password of the user * - * @return DataResponse<Http::STATUS_OK, array{lastLogin: int}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array<empty>, array{}> + * @return DataResponse<Http::STATUS_OK, array{lastLogin: int}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, list<empty>, array{}> * * 200: Password confirmation succeeded * 403: Password confirmation failed */ + #[NoAdminRequired] + #[BruteForceProtection(action: 'sudo')] #[UseSession] #[NoCSRFRequired] #[FrontpageRoute(verb: 'POST', url: '/login/confirm')] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] public function confirmPassword(string $password): DataResponse { $loginName = $this->userSession->getLoginName(); $loginResult = $this->userManager->checkPassword($loginName, $password); diff --git a/core/Controller/LostController.php b/core/Controller/LostController.php index d94386f9ab5..d956f3427f2 100644 --- a/core/Controller/LostController.php +++ b/core/Controller/LostController.php @@ -1,38 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Bernhard Posselt <dev@bernhard-posselt.com> - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Haertl <jus@bitgrid.net> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Rémy Jacquin <remy@remyj.fr> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Victor Dubiniuk <dubiniuk@owncloud.com> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Controller; @@ -43,9 +14,14 @@ use OC\Core\Events\PasswordResetEvent; use OC\Core\Exception\ResetPasswordException; use OC\Security\RateLimiting\Exception\RateLimitExceededException; use OC\Security\RateLimiting\Limiter; +use OC\User\Session; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\AnonRateLimit; +use OCP\AppFramework\Http\Attribute\BruteForceProtection; use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Services\IInitialState; @@ -61,8 +37,11 @@ use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserManager; use OCP\Mail\IMailer; +use OCP\PreConditionNotMetException; use OCP\Security\VerificationToken\InvalidTokenException; use OCP\Security\VerificationToken\IVerificationToken; +use OCP\Server; +use OCP\Util; use Psr\Log\LoggerInterface; use function array_filter; use function count; @@ -77,8 +56,6 @@ use function reset; */ #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] class LostController extends Controller { - protected string $from; - public function __construct( string $appName, IRequest $request, @@ -87,7 +64,7 @@ class LostController extends Controller { private Defaults $defaults, private IL10N $l10n, private IConfig $config, - string $defaultMailAddress, + protected string $defaultMailAddress, private IManager $encryptionManager, private IMailer $mailer, private LoggerInterface $logger, @@ -98,17 +75,15 @@ class LostController extends Controller { private Limiter $limiter, ) { parent::__construct($appName, $request); - $this->from = $defaultMailAddress; } /** * Someone wants to reset their password: - * - * @PublicPage - * @NoCSRFRequired - * @BruteForceProtection(action=passwordResetEmail) - * @AnonRateThrottle(limit=10, period=300) */ + #[PublicPage] + #[NoCSRFRequired] + #[BruteForceProtection(action: 'passwordResetEmail')] + #[AnonRateLimit(limit: 10, period: 300)] #[FrontpageRoute(verb: 'GET', url: '/lostpassword/reset/form/{token}/{userId}')] public function resetform(string $token, string $userId): TemplateResponse { try { @@ -120,7 +95,7 @@ class LostController extends Controller { ) { $response = new TemplateResponse( 'core', 'error', [ - "errors" => [["error" => $e->getMessage()]] + 'errors' => [['error' => $e->getMessage()]] ], TemplateResponse::RENDER_AS_GUEST ); @@ -169,11 +144,9 @@ class LostController extends Controller { return array_merge($data, ['status' => 'success']); } - /** - * @PublicPage - * @BruteForceProtection(action=passwordResetEmail) - * @AnonRateThrottle(limit=10, period=300) - */ + #[PublicPage] + #[BruteForceProtection(action: 'passwordResetEmail')] + #[AnonRateLimit(limit: 10, period: 300)] #[FrontpageRoute(verb: 'POST', url: '/lostpassword/email')] public function email(string $user): JSONResponse { if ($this->config->getSystemValue('lost_password_link', '') !== '') { @@ -186,7 +159,7 @@ class LostController extends Controller { return new JSONResponse($this->error($this->l10n->t('Unsupported email length (>255)'))); } - \OCP\Util::emitHook( + Util::emitHook( '\OCA\Files_Sharing\API\Server2Server', 'preLoginNameUsedAsUserName', ['uid' => &$user] @@ -207,11 +180,9 @@ class LostController extends Controller { return $response; } - /** - * @PublicPage - * @BruteForceProtection(action=passwordResetEmail) - * @AnonRateThrottle(limit=10, period=300) - */ + #[PublicPage] + #[BruteForceProtection(action: 'passwordResetEmail')] + #[AnonRateLimit(limit: 10, period: 300)] #[FrontpageRoute(verb: 'POST', url: '/lostpassword/set/{token}/{userId}')] public function setPassword(string $token, string $userId, string $password, bool $proceed): JSONResponse { if ($this->encryptionManager->isEnabled() && !$proceed) { @@ -247,7 +218,7 @@ class LostController extends Controller { $this->twoFactorManager->clearTwoFactorPending($userId); $this->config->deleteUserValue($userId, 'core', 'lostpassword'); - @\OC::$server->getUserSession()->unsetMagicInCookie(); + @Server::get(Session::class)->unsetMagicInCookie(); } catch (HintException $e) { $response = new JSONResponse($this->error($e->getHint())); $response->throttle(); @@ -263,7 +234,7 @@ class LostController extends Controller { /** * @throws ResetPasswordException - * @throws \OCP\PreConditionNotMetException + * @throws PreConditionNotMetException */ protected function sendEmail(string $input): void { $user = $this->findUserByIdOrMail($input); @@ -310,7 +281,7 @@ class LostController extends Controller { try { $message = $this->mailer->createMessage(); $message->setTo([$email => $user->getDisplayName()]); - $message->setFrom([$this->from => $this->defaults->getName()]); + $message->setFrom([$this->defaultMailAddress => $this->defaults->getName()]); $message->useTemplate($emailTemplate); $this->mailer->send($message); } catch (Exception $e) { diff --git a/core/Controller/NavigationController.php b/core/Controller/NavigationController.php index 7b651e6ec70..017061ef979 100644 --- a/core/Controller/NavigationController.php +++ b/core/Controller/NavigationController.php @@ -1,31 +1,16 @@ <?php + /** - * @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net> - * - * @author Julius Härtl <jus@bitgrid.net> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; -use OCA\Core\ResponseDefinitions; +use OC\Core\ResponseDefinitions; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; use OCP\INavigationManager; @@ -46,17 +31,16 @@ class NavigationController extends OCSController { } /** - * @NoAdminRequired - * @NoCSRFRequired - * * Get the apps navigation * * @param bool $absolute Rewrite URLs to absolute ones - * @return DataResponse<Http::STATUS_OK, CoreNavigationEntry[], array{}>|DataResponse<Http::STATUS_NOT_MODIFIED, array<empty>, array{}> + * @return DataResponse<Http::STATUS_OK, list<CoreNavigationEntry>, array{}>|DataResponse<Http::STATUS_NOT_MODIFIED, list<empty>, array{}> * * 200: Apps navigation returned * 304: No apps navigation changed */ + #[NoAdminRequired] + #[NoCSRFRequired] #[ApiRoute(verb: 'GET', url: '/navigation/apps', root: '/core')] public function getAppsNavigation(bool $absolute = false): DataResponse { $navigation = $this->navigationManager->getAll(); @@ -64,27 +48,22 @@ class NavigationController extends OCSController { $navigation = $this->rewriteToAbsoluteUrls($navigation); } $navigation = array_values($navigation); - $etag = $this->generateETag($navigation); - if ($this->request->getHeader('If-None-Match') === $etag) { - return new DataResponse([], Http::STATUS_NOT_MODIFIED); - } $response = new DataResponse($navigation); - $response->setETag($etag); + $response->setETag($this->generateETag($navigation)); return $response; } /** - * @NoAdminRequired - * @NoCSRFRequired - * * Get the settings navigation * * @param bool $absolute Rewrite URLs to absolute ones - * @return DataResponse<Http::STATUS_OK, CoreNavigationEntry[], array{}>|DataResponse<Http::STATUS_NOT_MODIFIED, array<empty>, array{}> + * @return DataResponse<Http::STATUS_OK, list<CoreNavigationEntry>, array{}>|DataResponse<Http::STATUS_NOT_MODIFIED, list<empty>, array{}> * * 200: Apps navigation returned * 304: No apps navigation changed */ + #[NoAdminRequired] + #[NoCSRFRequired] #[ApiRoute(verb: 'GET', url: '/navigation/settings', root: '/core')] public function getSettingsNavigation(bool $absolute = false): DataResponse { $navigation = $this->navigationManager->getAll('settings'); @@ -92,12 +71,8 @@ class NavigationController extends OCSController { $navigation = $this->rewriteToAbsoluteUrls($navigation); } $navigation = array_values($navigation); - $etag = $this->generateETag($navigation); - if ($this->request->getHeader('If-None-Match') === $etag) { - return new DataResponse([], Http::STATUS_NOT_MODIFIED); - } $response = new DataResponse($navigation); - $response->setETag($etag); + $response->setETag($this->generateETag($navigation)); return $response; } diff --git a/core/Controller/OCJSController.php b/core/Controller/OCJSController.php index dbb203e827f..083ad4b209f 100644 --- a/core/Controller/OCJSController.php +++ b/core/Controller/OCJSController.php @@ -1,43 +1,26 @@ <?php + /** - * @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; use bantu\IniGetWrapper\IniGetWrapper; +use OC\Authentication\Token\IProvider; use OC\CapabilitiesManager; +use OC\Files\FilenameValidator; use OC\Template\JSConfigHelper; use OCP\App\IAppManager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\DataDisplayResponse; use OCP\Defaults; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IGroupManager; use OCP\IInitialStateService; @@ -46,6 +29,7 @@ use OCP\ISession; use OCP\IURLGenerator; use OCP\IUserSession; use OCP\L10N\IFactory; +use OCP\ServerVersion; #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] class OCJSController extends Controller { @@ -60,34 +44,42 @@ class OCJSController extends Controller { ISession $session, IUserSession $userSession, IConfig $config, + IAppConfig $appConfig, IGroupManager $groupManager, IniGetWrapper $iniWrapper, IURLGenerator $urlGenerator, CapabilitiesManager $capabilitiesManager, IInitialStateService $initialStateService, + IProvider $tokenProvider, + FilenameValidator $filenameValidator, + ServerVersion $serverVersion, ) { parent::__construct($appName, $request); $this->helper = new JSConfigHelper( + $serverVersion, $l10nFactory->get('lib'), $defaults, $appManager, $session, $userSession->getUser(), $config, + $appConfig, $groupManager, $iniWrapper, $urlGenerator, $capabilitiesManager, - $initialStateService + $initialStateService, + $tokenProvider, + $filenameValidator, ); } /** - * @NoCSRFRequired * @NoTwoFactorRequired - * @PublicPage */ + #[PublicPage] + #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: '/core/js/oc.js')] public function getConfig(): DataDisplayResponse { $data = $this->helper->getConfig(); diff --git a/core/Controller/OCMController.php b/core/Controller/OCMController.php index 4fa03e67c4f..2d3b99f431d 100644 --- a/core/Controller/OCMController.php +++ b/core/Controller/OCMController.php @@ -3,36 +3,23 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2023 Maxence Lange <maxence@artificial-owl.com> - * - * @author Maxence Lange <maxence@artificial-owl.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; use Exception; +use OCA\CloudFederationAPI\Capabilities; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\DataResponse; use OCP\Capabilities\ICapability; -use OCP\IConfig; +use OCP\IAppConfig; use OCP\IRequest; use OCP\Server; use Psr\Container\ContainerExceptionInterface; @@ -46,8 +33,8 @@ use Psr\Log\LoggerInterface; class OCMController extends Controller { public function __construct( IRequest $request, - private IConfig $config, - private LoggerInterface $logger + private readonly IAppConfig $appConfig, + private LoggerInterface $logger, ) { parent::__construct('core', $request); } @@ -56,23 +43,24 @@ class OCMController extends Controller { * generate a OCMProvider with local data and send it as DataResponse. * This replaces the old PHP file ocm-provider/index.php * - * @PublicPage - * @NoCSRFRequired * @psalm-suppress MoreSpecificReturnType * @psalm-suppress LessSpecificReturnStatement - * @return DataResponse<Http::STATUS_OK, array{enabled: bool, apiVersion: string, endPoint: string, resourceTypes: array{name: string, shareTypes: string[], protocols: array{webdav: string}}[]}, array{X-NEXTCLOUD-OCM-PROVIDERS: true, Content-Type: 'application/json'}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}> + * @return DataResponse<Http::STATUS_OK, array{enabled: bool, apiVersion: string, endPoint: string, resourceTypes: list<array{name: string, shareTypes: list<string>, protocols: array{webdav: string}}>}, array{X-NEXTCLOUD-OCM-PROVIDERS: true, Content-Type: 'application/json'}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}> * * 200: OCM Provider details returned * 500: OCM not supported */ + #[PublicPage] + #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: '/ocm-provider/')] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] public function discovery(): DataResponse { try { $cap = Server::get( - $this->config->getAppValue( - 'core', - 'ocm_providers', - '\OCA\CloudFederationAPI\Capabilities' + $this->appConfig->getValueString( + 'core', 'ocm_providers', + Capabilities::class, + lazy: true ) ); diff --git a/core/Controller/OCSController.php b/core/Controller/OCSController.php index c6ddc23717d..fb0280479c4 100644 --- a/core/Controller/OCSController.php +++ b/core/Controller/OCSController.php @@ -1,30 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; @@ -32,11 +10,15 @@ use OC\CapabilitiesManager; use OC\Security\IdentityProof\Manager; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\BruteForceProtection; use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\DataResponse; use OCP\IRequest; use OCP\IUserManager; use OCP\IUserSession; +use OCP\ServerVersion; +use OCP\Util; class OCSController extends \OCP\AppFramework\OCSController { public function __construct( @@ -46,13 +28,12 @@ class OCSController extends \OCP\AppFramework\OCSController { private IUserSession $userSession, private IUserManager $userManager, private Manager $keyManager, + private ServerVersion $serverVersion, ) { parent::__construct($appName, $request); } - /** - * @PublicPage - */ + #[PublicPage] #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] #[ApiRoute(verb: 'GET', url: '/config', root: '')] public function getConfig(): DataResponse { @@ -68,25 +49,23 @@ class OCSController extends \OCP\AppFramework\OCSController { } /** - * @PublicPage - * * Get the capabilities * * @return DataResponse<Http::STATUS_OK, array{version: array{major: int, minor: int, micro: int, string: string, edition: '', extendedSupport: bool}, capabilities: array<string, mixed>}, array{}> * * 200: Capabilities returned */ + #[PublicPage] #[ApiRoute(verb: 'GET', url: '/capabilities', root: '/cloud')] public function getCapabilities(): DataResponse { $result = []; - [$major, $minor, $micro] = \OCP\Util::getVersion(); $result['version'] = [ - 'major' => (int)$major, - 'minor' => (int)$minor, - 'micro' => (int)$micro, - 'string' => \OC_Util::getVersionString(), + 'major' => $this->serverVersion->getMajorVersion(), + 'minor' => $this->serverVersion->getMinorVersion(), + 'micro' => $this->serverVersion->getPatchVersion(), + 'string' => $this->serverVersion->getVersionString(), 'edition' => '', - 'extendedSupport' => \OCP\Util::hasExtendedSupport() + 'extendedSupport' => Util::hasExtendedSupport() ]; if ($this->userSession->isLoggedIn()) { @@ -100,10 +79,8 @@ class OCSController extends \OCP\AppFramework\OCSController { return $response; } - /** - * @PublicPage - * @BruteForceProtection(action=login) - */ + #[PublicPage] + #[BruteForceProtection(action: 'login')] #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] #[ApiRoute(verb: 'POST', url: '/check', root: '/person')] public function personCheck(string $login = '', string $password = ''): DataResponse { @@ -123,9 +100,7 @@ class OCSController extends \OCP\AppFramework\OCSController { return new DataResponse([], 101); } - /** - * @PublicPage - */ + #[PublicPage] #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] #[ApiRoute(verb: 'GET', url: '/key/{cloudId}', root: '/identityproof')] public function getIdentityProof(string $cloudId): DataResponse { diff --git a/core/Controller/PreviewController.php b/core/Controller/PreviewController.php index 7ab22dceaa2..aac49c06d57 100644 --- a/core/Controller/PreviewController.php +++ b/core/Controller/PreviewController.php @@ -3,41 +3,27 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; -use OCA\Files_Sharing\SharedStorage; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\FileDisplayResponse; use OCP\AppFramework\Http\RedirectResponse; +use OCP\AppFramework\Http\Response; use OCP\Files\File; use OCP\Files\IRootFolder; use OCP\Files\Node; use OCP\Files\NotFoundException; +use OCP\Files\Storage\ISharedStorage; use OCP\IPreview; use OCP\IRequest; use OCP\Preview\IMimeIconProvider; @@ -55,19 +41,16 @@ class PreviewController extends Controller { } /** - * @NoAdminRequired - * @NoCSRFRequired - * * Get a preview by file path * * @param string $file Path of the file - * @param int $x Width of the preview - * @param int $y Height of the preview - * @param bool $a Whether to not crop the preview + * @param int $x Width of the preview. A width of -1 will use the original image width. + * @param int $y Height of the preview. A height of -1 will use the original image height. + * @param bool $a Preserve the aspect ratio * @param bool $forceIcon Force returning an icon - * @param string $mode How to crop the image + * @param 'fill'|'cover' $mode How to crop the image * @param bool $mimeFallback Whether to fallback to the mime icon if no preview is available - * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array<empty>, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}> + * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, list<empty>, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}> * * 200: Preview returned * 303: Redirect to the mime icon url if mimeFallback is true @@ -75,7 +58,10 @@ class PreviewController extends Controller { * 403: Getting preview is not allowed * 404: Preview not found */ + #[NoAdminRequired] + #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: '/core/preview.png')] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] public function getPreview( string $file = '', int $x = 32, @@ -83,7 +69,7 @@ class PreviewController extends Controller { bool $a = false, bool $forceIcon = true, string $mode = 'fill', - bool $mimeFallback = false): Http\Response { + bool $mimeFallback = false): Response { if ($file === '' || $x === 0 || $y === 0) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } @@ -99,19 +85,16 @@ class PreviewController extends Controller { } /** - * @NoAdminRequired - * @NoCSRFRequired - * * Get a preview by file ID * * @param int $fileId ID of the file - * @param int $x Width of the preview - * @param int $y Height of the preview - * @param bool $a Whether to not crop the preview + * @param int $x Width of the preview. A width of -1 will use the original image width. + * @param int $y Height of the preview. A height of -1 will use the original image height. + * @param bool $a Preserve the aspect ratio * @param bool $forceIcon Force returning an icon - * @param string $mode How to crop the image + * @param 'fill'|'cover' $mode How to crop the image * @param bool $mimeFallback Whether to fallback to the mime icon if no preview is available - * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array<empty>, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}> + * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, list<empty>, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}> * * 200: Preview returned * 303: Redirect to the mime icon url if mimeFallback is true @@ -119,7 +102,10 @@ class PreviewController extends Controller { * 403: Getting preview is not allowed * 404: Preview not found */ + #[NoAdminRequired] + #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: '/core/preview')] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] public function getPreviewByFileId( int $fileId = -1, int $x = 32, @@ -143,7 +129,7 @@ class PreviewController extends Controller { } /** - * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array<empty>, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}> + * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, list<empty>, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}> */ private function fetchPreview( Node $node, @@ -152,7 +138,7 @@ class PreviewController extends Controller { bool $a, bool $forceIcon, string $mode, - bool $mimeFallback = false) : Http\Response { + bool $mimeFallback = false) : Response { if (!($node instanceof File) || (!$forceIcon && !$this->preview->isAvailable($node))) { return new DataResponse([], Http::STATUS_NOT_FOUND); } @@ -160,12 +146,18 @@ class PreviewController extends Controller { return new DataResponse([], Http::STATUS_FORBIDDEN); } + if ($node->getId() <= 0) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + // Is this header is set it means our UI is doing a preview for no-download shares + // we check a header so we at least prevent people from using the link directly (obfuscation) + $isNextcloudPreview = $this->request->getHeader('x-nc-preview') === 'true'; $storage = $node->getStorage(); - if ($storage->instanceOfStorage(SharedStorage::class)) { - /** @var SharedStorage $storage */ + if ($isNextcloudPreview === false && $storage->instanceOfStorage(ISharedStorage::class)) { + /** @var ISharedStorage $storage */ $share = $storage->getShare(); - $attributes = $share->getAttributes(); - if ($attributes !== null && $attributes->getAttribute('permissions', 'download') === false) { + if (!$share->canSeeContent()) { return new DataResponse([], Http::STATUS_FORBIDDEN); } } @@ -190,4 +182,25 @@ class PreviewController extends Controller { return new DataResponse([], Http::STATUS_BAD_REQUEST); } } + + /** + * Get a preview by mime + * + * @param string $mime Mime type + * @return RedirectResponse<Http::STATUS_SEE_OTHER, array{}> + * + * 303: The mime icon url + */ + #[NoCSRFRequired] + #[PublicPage] + #[FrontpageRoute(verb: 'GET', url: '/core/mimeicon')] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] + public function getMimeIconUrl(string $mime = 'application/octet-stream') { + $url = $this->mimeIconProvider->getMimeIconUrl($mime); + if ($url === null) { + $url = $this->mimeIconProvider->getMimeIconUrl('application/octet-stream'); + } + + return new RedirectResponse($url); + } } diff --git a/core/Controller/ProfileApiController.php b/core/Controller/ProfileApiController.php index 7cba0593c1f..02979cb1649 100644 --- a/core/Controller/ProfileApiController.php +++ b/core/Controller/ProfileApiController.php @@ -3,83 +3,79 @@ declare(strict_types=1); /** - * @copyright 2021 Christopher Ng <chrng8@gmail.com> - * - * @author Christopher Ng <chrng8@gmail.com> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; use OC\Core\Db\ProfileConfigMapper; +use OC\Core\ResponseDefinitions; use OC\Profile\ProfileManager; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\BruteForceProtection; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; +use OCP\AppFramework\Http\Attribute\UserRateLimit; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSBadRequestException; use OCP\AppFramework\OCS\OCSForbiddenException; use OCP\AppFramework\OCS\OCSNotFoundException; use OCP\AppFramework\OCSController; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; use OCP\IRequest; +use OCP\IUser; use OCP\IUserManager; use OCP\IUserSession; +use OCP\Share\IManager; +/** + * @psalm-import-type CoreProfileData from ResponseDefinitions + */ class ProfileApiController extends OCSController { public function __construct( IRequest $request, + private IConfig $config, + private ITimeFactory $timeFactory, private ProfileConfigMapper $configMapper, private ProfileManager $profileManager, private IUserManager $userManager, private IUserSession $userSession, + private IManager $shareManager, ) { parent::__construct('core', $request); } /** - * @NoAdminRequired * @NoSubAdminRequired - * @PasswordConfirmationRequired - * @UserRateThrottle(limit=40, period=600) * * Update the visibility of a parameter * * @param string $targetUserId ID of the user * @param string $paramId ID of the parameter * @param string $visibility New visibility - * @return DataResponse<Http::STATUS_OK, array<empty>, array{}> + * @return DataResponse<Http::STATUS_OK, list<empty>, array{}> * @throws OCSBadRequestException Updating visibility is not possible * @throws OCSForbiddenException Not allowed to edit other users visibility * @throws OCSNotFoundException Account not found * * 200: Visibility updated successfully */ + #[NoAdminRequired] + #[PasswordConfirmationRequired] + #[UserRateLimit(limit: 40, period: 600)] #[ApiRoute(verb: 'PUT', url: '/{targetUserId}', root: '/profile')] public function setVisibility(string $targetUserId, string $paramId, string $visibility): DataResponse { $requestingUser = $this->userSession->getUser(); - $targetUser = $this->userManager->get($targetUserId); - - if (!$this->userManager->userExists($targetUserId)) { - throw new OCSNotFoundException('Account does not exist'); + if ($requestingUser->getUID() !== $targetUserId) { + throw new OCSForbiddenException('People can only edit their own visibility settings'); } - if ($requestingUser !== $targetUser) { - throw new OCSForbiddenException('People can only edit their own visibility settings'); + $targetUser = $this->userManager->get($targetUserId); + if (!$targetUser instanceof IUser) { + throw new OCSNotFoundException('Account does not exist'); } // Ensure that a profile config is created in the database @@ -95,4 +91,55 @@ class ProfileApiController extends OCSController { return new DataResponse(); } + + /** + * Get profile fields for another user + * + * @param string $targetUserId ID of the user + * @return DataResponse<Http::STATUS_OK, CoreProfileData, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, null, array{}> + * + * 200: Profile data returned successfully + * 400: Profile is disabled + * 404: Account not found or disabled + */ + #[NoAdminRequired] + #[ApiRoute(verb: 'GET', url: '/{targetUserId}', root: '/profile')] + #[BruteForceProtection(action: 'user')] + #[UserRateLimit(limit: 30, period: 120)] + public function getProfileFields(string $targetUserId): DataResponse { + $targetUser = $this->userManager->get($targetUserId); + if (!$targetUser instanceof IUser) { + $response = new DataResponse(null, Http::STATUS_NOT_FOUND); + $response->throttle(); + return $response; + } + if (!$targetUser->isEnabled()) { + return new DataResponse(null, Http::STATUS_NOT_FOUND); + } + + if (!$this->profileManager->isProfileEnabled($targetUser)) { + return new DataResponse(null, Http::STATUS_BAD_REQUEST); + } + + $requestingUser = $this->userSession->getUser(); + if ($targetUser !== $requestingUser) { + if (!$this->shareManager->currentUserCanEnumerateTargetUser($requestingUser, $targetUser)) { + return new DataResponse(null, Http::STATUS_NOT_FOUND); + } + } + + $profileFields = $this->profileManager->getProfileFields($targetUser, $requestingUser); + + // Extend the profile information with timezone of the user + $timezoneStringTarget = $this->config->getUserValue($targetUser->getUID(), 'core', 'timezone') ?: $this->config->getSystemValueString('default_timezone', 'UTC'); + try { + $timezoneTarget = new \DateTimeZone($timezoneStringTarget); + } catch (\Throwable) { + $timezoneTarget = new \DateTimeZone('UTC'); + } + $profileFields['timezone'] = $timezoneTarget->getName(); // E.g. Europe/Berlin + $profileFields['timezoneOffset'] = $timezoneTarget->getOffset($this->timeFactory->now()); // In seconds E.g. 7200 + + return new DataResponse($profileFields); + } } diff --git a/core/Controller/ProfilePageController.php b/core/Controller/ProfilePageController.php deleted file mode 100644 index c3a33d6bbda..00000000000 --- a/core/Controller/ProfilePageController.php +++ /dev/null @@ -1,126 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @copyright 2021 Christopher Ng <chrng8@gmail.com> - * - * @author Christopher Ng <chrng8@gmail.com> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ - -namespace OC\Core\Controller; - -use OC\Profile\ProfileManager; -use OCP\AppFramework\Controller; -use OCP\AppFramework\Http\Attribute\FrontpageRoute; -use OCP\AppFramework\Http\Attribute\OpenAPI; -use OCP\AppFramework\Http\TemplateResponse; -use OCP\AppFramework\Services\IInitialState; -use OCP\EventDispatcher\IEventDispatcher; -use OCP\INavigationManager; -use OCP\IRequest; -use OCP\IUser; -use OCP\IUserManager; -use OCP\IUserSession; -use OCP\Profile\BeforeTemplateRenderedEvent; -use OCP\Share\IManager as IShareManager; -use OCP\UserStatus\IManager as IUserStatusManager; - -#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] -class ProfilePageController extends Controller { - public function __construct( - string $appName, - IRequest $request, - private IInitialState $initialStateService, - private ProfileManager $profileManager, - private IShareManager $shareManager, - private IUserManager $userManager, - private IUserSession $userSession, - private IUserStatusManager $userStatusManager, - private INavigationManager $navigationManager, - private IEventDispatcher $eventDispatcher, - ) { - parent::__construct($appName, $request); - } - - /** - * @PublicPage - * @NoCSRFRequired - * @NoAdminRequired - * @NoSubAdminRequired - */ - #[FrontpageRoute(verb: 'GET', url: '/u/{targetUserId}')] - public function index(string $targetUserId): TemplateResponse { - $profileNotFoundTemplate = new TemplateResponse( - 'core', - '404-profile', - [], - TemplateResponse::RENDER_AS_GUEST, - ); - - $targetUser = $this->userManager->get($targetUserId); - if (!($targetUser instanceof IUser) || !$targetUser->isEnabled()) { - return $profileNotFoundTemplate; - } - $visitingUser = $this->userSession->getUser(); - - if (!$this->profileManager->isProfileEnabled($targetUser)) { - return $profileNotFoundTemplate; - } - - // Run user enumeration checks only if viewing another user's profile - if ($targetUser !== $visitingUser) { - if (!$this->shareManager->currentUserCanEnumerateTargetUser($visitingUser, $targetUser)) { - return $profileNotFoundTemplate; - } - } - - if ($visitingUser !== null) { - $userStatuses = $this->userStatusManager->getUserStatuses([$targetUserId]); - $status = $userStatuses[$targetUserId] ?? null; - if ($status !== null) { - $this->initialStateService->provideInitialState('status', [ - 'icon' => $status->getIcon(), - 'message' => $status->getMessage(), - ]); - } - } - - $this->initialStateService->provideInitialState( - 'profileParameters', - $this->profileManager->getProfileFields($targetUser, $visitingUser), - ); - - if ($targetUser === $visitingUser) { - $this->navigationManager->setActiveEntry('profile'); - } - - $this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($targetUserId)); - - \OCP\Util::addScript('core', 'profile'); - - return new TemplateResponse( - 'core', - 'profile', - [], - $this->userSession->isLoggedIn() ? TemplateResponse::RENDER_AS_USER : TemplateResponse::RENDER_AS_PUBLIC, - ); - } -} diff --git a/core/Controller/RecommendedAppsController.php b/core/Controller/RecommendedAppsController.php index 5d4749e1e83..ba35bc8705e 100644 --- a/core/Controller/RecommendedAppsController.php +++ b/core/Controller/RecommendedAppsController.php @@ -3,31 +3,14 @@ declare(strict_types=1); /** - * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\Attribute\OpenAPI; use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\StandaloneTemplateResponse; @@ -46,9 +29,9 @@ class RecommendedAppsController extends Controller { } /** - * @NoCSRFRequired * @return Response */ + #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: '/core/apps/recommended')] public function index(): Response { $defaultPageUrl = $this->urlGenerator->linkToDefaultPageUrl(); diff --git a/core/Controller/ReferenceApiController.php b/core/Controller/ReferenceApiController.php index 854c15cb985..d4fb753f404 100644 --- a/core/Controller/ReferenceApiController.php +++ b/core/Controller/ReferenceApiController.php @@ -2,33 +2,20 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net> - * - * @author Julius Härtl <jus@bitgrid.net> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; -use OCA\Core\ResponseDefinitions; +use OC\Core\ResponseDefinitions; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\AnonRateLimit; use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCSController; use OCP\Collaboration\Reference\IDiscoverableReferenceProvider; use OCP\Collaboration\Reference\IReferenceManager; use OCP\Collaboration\Reference\Reference; @@ -38,7 +25,9 @@ use OCP\IRequest; * @psalm-import-type CoreReference from ResponseDefinitions * @psalm-import-type CoreReferenceProvider from ResponseDefinitions */ -class ReferenceApiController extends \OCP\AppFramework\OCSController { +class ReferenceApiController extends OCSController { + private const LIMIT_MAX = 15; + public function __construct( string $appName, IRequest $request, @@ -49,8 +38,6 @@ class ReferenceApiController extends \OCP\AppFramework\OCSController { } /** - * @NoAdminRequired - * * Extract references from a text * * @param string $text Text to extract from @@ -60,6 +47,7 @@ class ReferenceApiController extends \OCP\AppFramework\OCSController { * * 200: References returned */ + #[NoAdminRequired] #[ApiRoute(verb: 'POST', url: '/extract', root: '/references')] public function extract(string $text, bool $resolve = false, int $limit = 1): DataResponse { $references = $this->referenceManager->extractReferences($text); @@ -80,8 +68,38 @@ class ReferenceApiController extends \OCP\AppFramework\OCSController { } /** - * @NoAdminRequired + * Extract references from a text + * + * @param string $text Text to extract from + * @param string $sharingToken Token of the public share + * @param bool $resolve Resolve the references + * @param int $limit Maximum amount of references to extract, limited to 15 + * @return DataResponse<Http::STATUS_OK, array{references: array<string, CoreReference|null>}, array{}> * + * 200: References returned + */ + #[ApiRoute(verb: 'POST', url: '/extractPublic', root: '/references')] + #[PublicPage] + #[AnonRateLimit(limit: 10, period: 120)] + public function extractPublic(string $text, string $sharingToken, bool $resolve = false, int $limit = 1): DataResponse { + $references = $this->referenceManager->extractReferences($text); + + $result = []; + $index = 0; + foreach ($references as $reference) { + if ($index++ >= min($limit, self::LIMIT_MAX)) { + break; + } + + $result[$reference] = $resolve ? $this->referenceManager->resolveReference($reference, true, $sharingToken)?->jsonSerialize() : null; + } + + return new DataResponse([ + 'references' => $result + ]); + } + + /** * Resolve a reference * * @param string $reference Reference to resolve @@ -89,6 +107,7 @@ class ReferenceApiController extends \OCP\AppFramework\OCSController { * * 200: Reference returned */ + #[NoAdminRequired] #[ApiRoute(verb: 'GET', url: '/resolve', root: '/references')] public function resolveOne(string $reference): DataResponse { /** @var ?CoreReference $resolvedReference */ @@ -100,16 +119,36 @@ class ReferenceApiController extends \OCP\AppFramework\OCSController { } /** - * @NoAdminRequired + * Resolve from a public page * + * @param string $reference Reference to resolve + * @param string $sharingToken Token of the public share + * @return DataResponse<Http::STATUS_OK, array{references: array<string, ?CoreReference>}, array{}> + * + * 200: Reference returned + */ + #[ApiRoute(verb: 'GET', url: '/resolvePublic', root: '/references')] + #[PublicPage] + #[AnonRateLimit(limit: 25, period: 120)] + public function resolveOnePublic(string $reference, string $sharingToken): DataResponse { + /** @var ?CoreReference $resolvedReference */ + $resolvedReference = $this->referenceManager->resolveReference(trim($reference), true, trim($sharingToken))?->jsonSerialize(); + + $response = new DataResponse(['references' => [$reference => $resolvedReference]]); + $response->cacheFor(3600, false, true); + return $response; + } + + /** * Resolve multiple references * - * @param string[] $references References to resolve + * @param list<string> $references References to resolve * @param int $limit Maximum amount of references to resolve * @return DataResponse<Http::STATUS_OK, array{references: array<string, CoreReference|null>}, array{}> * * 200: References returned */ + #[NoAdminRequired] #[ApiRoute(verb: 'POST', url: '/resolve', root: '/references')] public function resolve(array $references, int $limit = 1): DataResponse { $result = []; @@ -128,26 +167,52 @@ class ReferenceApiController extends \OCP\AppFramework\OCSController { } /** - * @NoAdminRequired + * Resolve multiple references from a public page + * + * @param list<string> $references References to resolve + * @param string $sharingToken Token of the public share + * @param int $limit Maximum amount of references to resolve, limited to 15 + * @return DataResponse<Http::STATUS_OK, array{references: array<string, CoreReference|null>}, array{}> * + * 200: References returned + */ + #[ApiRoute(verb: 'POST', url: '/resolvePublic', root: '/references')] + #[PublicPage] + #[AnonRateLimit(limit: 10, period: 120)] + public function resolvePublic(array $references, string $sharingToken, int $limit = 1): DataResponse { + $result = []; + $index = 0; + foreach ($references as $reference) { + if ($index++ >= min($limit, self::LIMIT_MAX)) { + break; + } + + $result[$reference] = $this->referenceManager->resolveReference($reference, true, $sharingToken)?->jsonSerialize(); + } + + return new DataResponse([ + 'references' => $result + ]); + } + + /** * Get the providers * - * @return DataResponse<Http::STATUS_OK, CoreReferenceProvider[], array{}> + * @return DataResponse<Http::STATUS_OK, list<CoreReferenceProvider>, array{}> * * 200: Providers returned */ + #[NoAdminRequired] #[ApiRoute(verb: 'GET', url: '/providers', root: '/references')] public function getProvidersInfo(): DataResponse { $providers = $this->referenceManager->getDiscoverableProviders(); - $jsonProviders = array_map(static function (IDiscoverableReferenceProvider $provider) { + $jsonProviders = array_values(array_map(static function (IDiscoverableReferenceProvider $provider) { return $provider->jsonSerialize(); - }, $providers); + }, $providers)); return new DataResponse($jsonProviders); } /** - * @NoAdminRequired - * * Touch a provider * * @param string $providerId ID of the provider @@ -156,6 +221,7 @@ class ReferenceApiController extends \OCP\AppFramework\OCSController { * * 200: Provider touched */ + #[NoAdminRequired] #[ApiRoute(verb: 'PUT', url: '/provider/{providerId}', root: '/references')] public function touchProvider(string $providerId, ?int $timestamp = null): DataResponse { if ($this->userId !== null) { diff --git a/core/Controller/ReferenceController.php b/core/Controller/ReferenceController.php index 8874978037f..6ed15e2d2f1 100644 --- a/core/Controller/ReferenceController.php +++ b/core/Controller/ReferenceController.php @@ -2,25 +2,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net> - * - * @author Julius Härtl <jus@bitgrid.net> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; @@ -28,6 +11,9 @@ namespace OC\Core\Controller; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\DataDownloadResponse; use OCP\AppFramework\Http\DataResponse; use OCP\Collaboration\Reference\IReferenceManager; @@ -47,9 +33,6 @@ class ReferenceController extends Controller { } /** - * @PublicPage - * @NoCSRFRequired - * * Get a preview for a reference * * @param string $referenceId the reference cache key @@ -58,7 +41,10 @@ class ReferenceController extends Controller { * 200: Preview returned * 404: Reference not found */ + #[PublicPage] + #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: '/core/references/preview/{referenceId}')] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] public function preview(string $referenceId): DataDownloadResponse|DataResponse { $reference = $this->referenceManager->getReferenceByCacheKey($referenceId); diff --git a/core/Controller/SearchController.php b/core/Controller/SearchController.php deleted file mode 100644 index ccea067ae2c..00000000000 --- a/core/Controller/SearchController.php +++ /dev/null @@ -1,65 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ -namespace OC\Core\Controller; - -use OCP\AppFramework\Controller; -use OCP\AppFramework\Http\Attribute\FrontpageRoute; -use OCP\AppFramework\Http\JSONResponse; -use OCP\IRequest; -use OCP\ISearch; -use OCP\Search\Result; -use Psr\Log\LoggerInterface; - -class SearchController extends Controller { - public function __construct( - string $appName, - IRequest $request, - private ISearch $searcher, - private LoggerInterface $logger, - ) { - parent::__construct($appName, $request); - } - - /** - * @NoAdminRequired - */ - #[FrontpageRoute(verb: 'GET', url: '/core/search')] - public function search(string $query, array $inApps = [], int $page = 1, int $size = 30): JSONResponse { - $results = $this->searcher->searchPaged($query, $inApps, $page, $size); - - $results = array_filter($results, function (Result $result) { - if (json_encode($result, JSON_HEX_TAG) === false) { - $this->logger->warning("Skipping search result due to invalid encoding: {type: " . $result->type . ", id: " . $result->id . "}"); - return false; - } else { - return true; - } - }); - - return new JSONResponse($results); - } -} diff --git a/core/Controller/SetupController.php b/core/Controller/SetupController.php index 0f1fa1c495e..f89506680ad 100644 --- a/core/Controller/SetupController.php +++ b/core/Controller/SetupController.php @@ -1,37 +1,18 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Bart Visscher <bartv@thisnet.nl> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Damjan Georgievski <gdamjan@gmail.com> - * @author ideaship <ideaship@users.noreply.github.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Controller; +use OC\IntegrityCheck\Checker; use OC\Setup; +use OCP\IInitialStateService; +use OCP\IURLGenerator; +use OCP\Server; +use OCP\Template\ITemplateManager; use OCP\Util; use Psr\Log\LoggerInterface; @@ -41,8 +22,11 @@ class SetupController { public function __construct( protected Setup $setupHelper, protected LoggerInterface $logger, + protected ITemplateManager $templateManager, + protected IInitialStateService $initialStateService, + protected IURLGenerator $urlGenerator, ) { - $this->autoConfigFile = \OC::$configDir.'autoconfig.php'; + $this->autoConfigFile = \OC::$configDir . 'autoconfig.php'; } public function run(array $post): void { @@ -81,10 +65,10 @@ class SetupController { } private function displaySetupForbidden(): void { - \OC_Template::printGuestPage('', 'installation_forbidden'); + $this->templateManager->printGuestPage('', 'installation_forbidden'); } - public function display($post): void { + public function display(array $post): void { $defaults = [ 'adminlogin' => '', 'adminpass' => '', @@ -94,6 +78,8 @@ class SetupController { 'dbtablespace' => '', 'dbhost' => 'localhost', 'dbtype' => '', + 'hasAutoconfig' => false, + 'serverRoot' => \OC::$SERVERROOT, ]; $parameters = array_merge($defaults, $post); @@ -102,30 +88,43 @@ class SetupController { // include common nextcloud webpack bundle Util::addScript('core', 'common'); Util::addScript('core', 'main'); + Util::addScript('core', 'install'); Util::addTranslations('core'); - \OC_Template::printGuestPage('', 'installation', $parameters); + $this->initialStateService->provideInitialState('core', 'config', $parameters); + $this->initialStateService->provideInitialState('core', 'data', false); + $this->initialStateService->provideInitialState('core', 'links', [ + 'adminInstall' => $this->urlGenerator->linkToDocs('admin-install'), + 'adminSourceInstall' => $this->urlGenerator->linkToDocs('admin-source_install'), + 'adminDBConfiguration' => $this->urlGenerator->linkToDocs('admin-db-configuration'), + ]); + + $this->templateManager->printGuestPage('', 'installation'); } private function finishSetup(): void { if (file_exists($this->autoConfigFile)) { unlink($this->autoConfigFile); } - \OC::$server->getIntegrityCodeChecker()->runInstanceVerification(); + Server::get(Checker::class)->runInstanceVerification(); if ($this->setupHelper->shouldRemoveCanInstallFile()) { - \OC_Template::printGuestPage('', 'installation_incomplete'); + $this->templateManager->printGuestPage('', 'installation_incomplete'); } - header('Location: ' . \OC::$server->getURLGenerator()->getAbsoluteURL('index.php/core/apps/recommended')); + header('Location: ' . Server::get(IURLGenerator::class)->getAbsoluteURL('index.php/core/apps/recommended')); exit(); } + /** + * @psalm-taint-escape file we trust file path given in POST for setup + */ public function loadAutoConfig(array $post): array { if (file_exists($this->autoConfigFile)) { $this->logger->info('Autoconfig file found, setting up Nextcloud…'); $AUTOCONFIG = []; include $this->autoConfigFile; + $post['hasAutoconfig'] = count($AUTOCONFIG) > 0; $post = array_merge($post, $AUTOCONFIG); } @@ -136,8 +135,6 @@ class SetupController { if ($dbIsSet and $directoryIsSet and $adminAccountIsSet) { $post['install'] = 'true'; } - $post['dbIsSet'] = $dbIsSet; - $post['directoryIsSet'] = $directoryIsSet; return $post; } diff --git a/core/Controller/TaskProcessingApiController.php b/core/Controller/TaskProcessingApiController.php new file mode 100644 index 00000000000..90a0e9ba14a --- /dev/null +++ b/core/Controller/TaskProcessingApiController.php @@ -0,0 +1,657 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + + +namespace OC\Core\Controller; + +use OC\Core\ResponseDefinitions; +use OC\Files\SimpleFS\SimpleFile; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\AnonRateLimit; +use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\ExAppRequired; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\PublicPage; +use OCP\AppFramework\Http\Attribute\UserRateLimit; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\StreamResponse; +use OCP\AppFramework\OCSController; +use OCP\Files\File; +use OCP\Files\IAppData; +use OCP\Files\IMimeTypeDetector; +use OCP\Files\IRootFolder; +use OCP\Files\NotPermittedException; +use OCP\IL10N; +use OCP\IRequest; +use OCP\Lock\LockedException; +use OCP\TaskProcessing\EShapeType; +use OCP\TaskProcessing\Exception\Exception; +use OCP\TaskProcessing\Exception\NotFoundException; +use OCP\TaskProcessing\Exception\PreConditionNotMetException; +use OCP\TaskProcessing\Exception\UnauthorizedException; +use OCP\TaskProcessing\Exception\ValidationException; +use OCP\TaskProcessing\IManager; +use OCP\TaskProcessing\ShapeEnumValue; +use OCP\TaskProcessing\Task; +use RuntimeException; +use stdClass; + +/** + * @psalm-import-type CoreTaskProcessingTask from ResponseDefinitions + * @psalm-import-type CoreTaskProcessingTaskType from ResponseDefinitions + */ +class TaskProcessingApiController extends OCSController { + public function __construct( + string $appName, + IRequest $request, + private IManager $taskProcessingManager, + private IL10N $l, + private ?string $userId, + private IRootFolder $rootFolder, + private IAppData $appData, + private IMimeTypeDetector $mimeTypeDetector, + ) { + parent::__construct($appName, $request); + } + + /** + * Returns all available TaskProcessing task types + * + * @return DataResponse<Http::STATUS_OK, array{types: array<string, CoreTaskProcessingTaskType>}, array{}> + * + * 200: Task types returned + */ + #[PublicPage] + #[ApiRoute(verb: 'GET', url: '/tasktypes', root: '/taskprocessing')] + public function taskTypes(): DataResponse { + /** @var array<string, CoreTaskProcessingTaskType> $taskTypes */ + $taskTypes = array_map(function (array $tt) { + $tt['inputShape'] = array_map(function ($descriptor) { + return $descriptor->jsonSerialize(); + }, $tt['inputShape']); + if (empty($tt['inputShape'])) { + $tt['inputShape'] = new stdClass; + } + + $tt['outputShape'] = array_map(function ($descriptor) { + return $descriptor->jsonSerialize(); + }, $tt['outputShape']); + if (empty($tt['outputShape'])) { + $tt['outputShape'] = new stdClass; + } + + $tt['optionalInputShape'] = array_map(function ($descriptor) { + return $descriptor->jsonSerialize(); + }, $tt['optionalInputShape']); + if (empty($tt['optionalInputShape'])) { + $tt['optionalInputShape'] = new stdClass; + } + + $tt['optionalOutputShape'] = array_map(function ($descriptor) { + return $descriptor->jsonSerialize(); + }, $tt['optionalOutputShape']); + if (empty($tt['optionalOutputShape'])) { + $tt['optionalOutputShape'] = new stdClass; + } + + $tt['inputShapeEnumValues'] = array_map(function (array $enumValues) { + return array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues); + }, $tt['inputShapeEnumValues']); + if (empty($tt['inputShapeEnumValues'])) { + $tt['inputShapeEnumValues'] = new stdClass; + } + + $tt['optionalInputShapeEnumValues'] = array_map(function (array $enumValues) { + return array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues); + }, $tt['optionalInputShapeEnumValues']); + if (empty($tt['optionalInputShapeEnumValues'])) { + $tt['optionalInputShapeEnumValues'] = new stdClass; + } + + $tt['outputShapeEnumValues'] = array_map(function (array $enumValues) { + return array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues); + }, $tt['outputShapeEnumValues']); + if (empty($tt['outputShapeEnumValues'])) { + $tt['outputShapeEnumValues'] = new stdClass; + } + + $tt['optionalOutputShapeEnumValues'] = array_map(function (array $enumValues) { + return array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues); + }, $tt['optionalOutputShapeEnumValues']); + if (empty($tt['optionalOutputShapeEnumValues'])) { + $tt['optionalOutputShapeEnumValues'] = new stdClass; + } + + if (empty($tt['inputShapeDefaults'])) { + $tt['inputShapeDefaults'] = new stdClass; + } + if (empty($tt['optionalInputShapeDefaults'])) { + $tt['optionalInputShapeDefaults'] = new stdClass; + } + return $tt; + }, $this->taskProcessingManager->getAvailableTaskTypes()); + return new DataResponse([ + 'types' => $taskTypes, + ]); + } + + /** + * Schedules a task + * + * @param array<string, mixed> $input Task's input parameters + * @param string $type Type of the task + * @param string $appId ID of the app that will execute the task + * @param string $customId An arbitrary identifier for the task + * @param string|null $webhookUri URI to be requested when the task finishes + * @param string|null $webhookMethod Method used for the webhook request (HTTP:GET, HTTP:POST, HTTP:PUT, HTTP:DELETE or AppAPI:APP_ID:GET, AppAPI:APP_ID:POST...) + * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_BAD_REQUEST|Http::STATUS_PRECONDITION_FAILED|Http::STATUS_UNAUTHORIZED, array{message: string}, array{}> + * + * 200: Task scheduled successfully + * 400: Scheduling task is not possible + * 412: Scheduling task is not possible + * 401: Cannot schedule task because it references files in its input that the user doesn't have access to + */ + #[PublicPage] + #[UserRateLimit(limit: 20, period: 120)] + #[AnonRateLimit(limit: 5, period: 120)] + #[ApiRoute(verb: 'POST', url: '/schedule', root: '/taskprocessing')] + public function schedule( + array $input, string $type, string $appId, string $customId = '', + ?string $webhookUri = null, ?string $webhookMethod = null, + ): DataResponse { + $task = new Task($type, $input, $appId, $this->userId, $customId); + $task->setWebhookUri($webhookUri); + $task->setWebhookMethod($webhookMethod); + try { + $this->taskProcessingManager->scheduleTask($task); + + /** @var CoreTaskProcessingTask $json */ + $json = $task->jsonSerialize(); + + return new DataResponse([ + 'task' => $json, + ]); + } catch (PreConditionNotMetException) { + return new DataResponse(['message' => $this->l->t('The given provider is not available')], Http::STATUS_PRECONDITION_FAILED); + } catch (ValidationException $e) { + return new DataResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST); + } catch (UnauthorizedException) { + return new DataResponse(['message' => 'User does not have access to the files mentioned in the task input'], Http::STATUS_UNAUTHORIZED); + } catch (Exception) { + return new DataResponse(['message' => 'Internal server error'], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + /** + * Gets a task including status and result + * + * Tasks are removed 1 week after receiving their last update + * + * @param int $id The id of the task + * + * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}> + * + * 200: Task returned + * 404: Task not found + */ + #[PublicPage] + #[ApiRoute(verb: 'GET', url: '/task/{id}', root: '/taskprocessing')] + public function getTask(int $id): DataResponse { + try { + $task = $this->taskProcessingManager->getUserTask($id, $this->userId); + + /** @var CoreTaskProcessingTask $json */ + $json = $task->jsonSerialize(); + + return new DataResponse([ + 'task' => $json, + ]); + } catch (NotFoundException) { + return new DataResponse(['message' => $this->l->t('Task not found')], Http::STATUS_NOT_FOUND); + } catch (RuntimeException) { + return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + /** + * Deletes a task + * + * @param int $id The id of the task + * + * @return DataResponse<Http::STATUS_OK, null, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}> + * + * 200: Task deleted + */ + #[NoAdminRequired] + #[ApiRoute(verb: 'DELETE', url: '/task/{id}', root: '/taskprocessing')] + public function deleteTask(int $id): DataResponse { + try { + $task = $this->taskProcessingManager->getUserTask($id, $this->userId); + + $this->taskProcessingManager->deleteTask($task); + + return new DataResponse(null); + } catch (NotFoundException) { + return new DataResponse(null); + } catch (Exception) { + return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + + /** + * Returns tasks for the current user filtered by the appId and optional customId + * + * @param string $appId ID of the app + * @param string|null $customId An arbitrary identifier for the task + * @return DataResponse<Http::STATUS_OK, array{tasks: list<CoreTaskProcessingTask>}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}> + * + * 200: Tasks returned + */ + #[NoAdminRequired] + #[ApiRoute(verb: 'GET', url: '/tasks/app/{appId}', root: '/taskprocessing')] + public function listTasksByApp(string $appId, ?string $customId = null): DataResponse { + try { + $tasks = $this->taskProcessingManager->getUserTasksByApp($this->userId, $appId, $customId); + $json = array_map(static function (Task $task) { + return $task->jsonSerialize(); + }, $tasks); + + return new DataResponse([ + 'tasks' => $json, + ]); + } catch (Exception) { + return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + /** + * Returns tasks for the current user filtered by the optional taskType and optional customId + * + * @param string|null $taskType The task type to filter by + * @param string|null $customId An arbitrary identifier for the task + * @return DataResponse<Http::STATUS_OK, array{tasks: list<CoreTaskProcessingTask>}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}> + * + * 200: Tasks returned + */ + #[NoAdminRequired] + #[ApiRoute(verb: 'GET', url: '/tasks', root: '/taskprocessing')] + public function listTasks(?string $taskType, ?string $customId = null): DataResponse { + try { + $tasks = $this->taskProcessingManager->getUserTasks($this->userId, $taskType, $customId); + $json = array_map(static function (Task $task) { + return $task->jsonSerialize(); + }, $tasks); + + return new DataResponse([ + 'tasks' => $json, + ]); + } catch (Exception) { + return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + /** + * Returns the contents of a file referenced in a task + * + * @param int $taskId The id of the task + * @param int $fileId The file id of the file to retrieve + * @return StreamResponse<Http::STATUS_OK, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}> + * + * 200: File content returned + * 404: Task or file not found + */ + #[NoAdminRequired] + #[NoCSRFRequired] + #[ApiRoute(verb: 'GET', url: '/tasks/{taskId}/file/{fileId}', root: '/taskprocessing')] + public function getFileContents(int $taskId, int $fileId): StreamResponse|DataResponse { + try { + $task = $this->taskProcessingManager->getUserTask($taskId, $this->userId); + return $this->getFileContentsInternal($task, $fileId); + } catch (NotFoundException) { + return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND); + } catch (LockedException) { + return new DataResponse(['message' => $this->l->t('Node is locked')], Http::STATUS_INTERNAL_SERVER_ERROR); + } catch (Exception) { + return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + /** + * Returns the contents of a file referenced in a task(ExApp route version) + * + * @param int $taskId The id of the task + * @param int $fileId The file id of the file to retrieve + * @return StreamResponse<Http::STATUS_OK, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}> + * + * 200: File content returned + * 404: Task or file not found + */ + #[ExAppRequired] + #[ApiRoute(verb: 'GET', url: '/tasks_provider/{taskId}/file/{fileId}', root: '/taskprocessing')] + public function getFileContentsExApp(int $taskId, int $fileId): StreamResponse|DataResponse { + try { + $task = $this->taskProcessingManager->getTask($taskId); + return $this->getFileContentsInternal($task, $fileId); + } catch (NotFoundException) { + return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND); + } catch (LockedException) { + return new DataResponse(['message' => $this->l->t('Node is locked')], Http::STATUS_INTERNAL_SERVER_ERROR); + } catch (Exception) { + return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + /** + * Upload a file so it can be referenced in a task result (ExApp route version) + * + * Use field 'file' for the file upload + * + * @param int $taskId The id of the task + * @return DataResponse<Http::STATUS_CREATED, array{fileId: int}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}> + * + * 201: File created + * 400: File upload failed or no file was uploaded + * 404: Task not found + */ + #[ExAppRequired] + #[ApiRoute(verb: 'POST', url: '/tasks_provider/{taskId}/file', root: '/taskprocessing')] + public function setFileContentsExApp(int $taskId): DataResponse { + try { + $task = $this->taskProcessingManager->getTask($taskId); + $file = $this->request->getUploadedFile('file'); + if (!isset($file['tmp_name'])) { + return new DataResponse(['message' => $this->l->t('Bad request')], Http::STATUS_BAD_REQUEST); + } + $handle = fopen($file['tmp_name'], 'r'); + if (!$handle) { + return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR); + } + $fileId = $this->setFileContentsInternal($handle); + return new DataResponse(['fileId' => $fileId], Http::STATUS_CREATED); + } catch (NotFoundException) { + return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND); + } catch (Exception) { + return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + /** + * @throws NotPermittedException + * @throws NotFoundException + * @throws LockedException + * + * @return StreamResponse<Http::STATUS_OK, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}> + */ + private function getFileContentsInternal(Task $task, int $fileId): StreamResponse|DataResponse { + $ids = $this->extractFileIdsFromTask($task); + if (!in_array($fileId, $ids)) { + return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND); + } + if ($task->getUserId() !== null) { + \OC_Util::setupFS($task->getUserId()); + } + $node = $this->rootFolder->getFirstNodeById($fileId); + if ($node === null) { + $node = $this->rootFolder->getFirstNodeByIdInPath($fileId, '/' . $this->rootFolder->getAppDataDirectoryName() . '/'); + if (!$node instanceof File) { + throw new NotFoundException('Node is not a file'); + } + } elseif (!$node instanceof File) { + throw new NotFoundException('Node is not a file'); + } + + $contentType = $node->getMimeType(); + if (function_exists('mime_content_type')) { + $mimeType = mime_content_type($node->fopen('rb')); + if ($mimeType !== false) { + $mimeType = $this->mimeTypeDetector->getSecureMimeType($mimeType); + if ($mimeType !== 'application/octet-stream') { + $contentType = $mimeType; + } + } + } + + $response = new StreamResponse($node->fopen('rb')); + $response->addHeader( + 'Content-Disposition', + 'attachment; filename="' . rawurldecode($node->getName()) . '"' + ); + $response->addHeader('Content-Type', $contentType); + return $response; + } + + /** + * @param Task $task + * @return list<int> + * @throws NotFoundException + */ + private function extractFileIdsFromTask(Task $task): array { + $ids = []; + $taskTypes = $this->taskProcessingManager->getAvailableTaskTypes(); + if (!isset($taskTypes[$task->getTaskTypeId()])) { + throw new NotFoundException('Could not find task type'); + } + $taskType = $taskTypes[$task->getTaskTypeId()]; + foreach ($taskType['inputShape'] + $taskType['optionalInputShape'] as $key => $descriptor) { + if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) { + /** @var int|list<int> $inputSlot */ + $inputSlot = $task->getInput()[$key]; + if (is_array($inputSlot)) { + $ids = array_merge($inputSlot, $ids); + } else { + $ids[] = $inputSlot; + } + } + } + if ($task->getOutput() !== null) { + foreach ($taskType['outputShape'] + $taskType['optionalOutputShape'] as $key => $descriptor) { + if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) { + /** @var int|list<int> $outputSlot */ + $outputSlot = $task->getOutput()[$key]; + if (is_array($outputSlot)) { + $ids = array_merge($outputSlot, $ids); + } else { + $ids[] = $outputSlot; + } + } + } + } + return $ids; + } + + /** + * Sets the task progress + * + * @param int $taskId The id of the task + * @param float $progress The progress + * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}> + * + * 200: Progress updated successfully + * 404: Task not found + */ + #[ExAppRequired] + #[ApiRoute(verb: 'POST', url: '/tasks_provider/{taskId}/progress', root: '/taskprocessing')] + public function setProgress(int $taskId, float $progress): DataResponse { + try { + $this->taskProcessingManager->setTaskProgress($taskId, $progress); + $task = $this->taskProcessingManager->getTask($taskId); + + /** @var CoreTaskProcessingTask $json */ + $json = $task->jsonSerialize(); + + return new DataResponse([ + 'task' => $json, + ]); + } catch (NotFoundException) { + return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND); + } catch (Exception) { + return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + /** + * Sets the task result + * + * @param int $taskId The id of the task + * @param array<string,mixed>|null $output The resulting task output, files are represented by their IDs + * @param string|null $errorMessage An error message if the task failed + * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}> + * + * 200: Result updated successfully + * 404: Task not found + */ + #[ExAppRequired] + #[ApiRoute(verb: 'POST', url: '/tasks_provider/{taskId}/result', root: '/taskprocessing')] + public function setResult(int $taskId, ?array $output = null, ?string $errorMessage = null): DataResponse { + try { + // set result + $this->taskProcessingManager->setTaskResult($taskId, $errorMessage, $output, true); + $task = $this->taskProcessingManager->getTask($taskId); + + /** @var CoreTaskProcessingTask $json */ + $json = $task->jsonSerialize(); + + return new DataResponse([ + 'task' => $json, + ]); + } catch (NotFoundException) { + return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND); + } catch (Exception) { + return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + /** + * Cancels a task + * + * @param int $taskId The id of the task + * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}> + * + * 200: Task canceled successfully + * 404: Task not found + */ + #[NoAdminRequired] + #[ApiRoute(verb: 'POST', url: '/tasks/{taskId}/cancel', root: '/taskprocessing')] + public function cancelTask(int $taskId): DataResponse { + try { + // Check if the current user can access the task + $this->taskProcessingManager->getUserTask($taskId, $this->userId); + // set result + $this->taskProcessingManager->cancelTask($taskId); + $task = $this->taskProcessingManager->getUserTask($taskId, $this->userId); + + /** @var CoreTaskProcessingTask $json */ + $json = $task->jsonSerialize(); + + return new DataResponse([ + 'task' => $json, + ]); + } catch (NotFoundException) { + return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND); + } catch (Exception) { + return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + /** + * Returns the next scheduled task for the taskTypeId + * + * @param list<string> $providerIds The ids of the providers + * @param list<string> $taskTypeIds The ids of the task types + * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask, provider: array{name: string}}, array{}>|DataResponse<Http::STATUS_NO_CONTENT, null, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}> + * + * 200: Task returned + * 204: No task found + */ + #[ExAppRequired] + #[ApiRoute(verb: 'GET', url: '/tasks_provider/next', root: '/taskprocessing')] + public function getNextScheduledTask(array $providerIds, array $taskTypeIds): DataResponse { + try { + $providerIdsBasedOnTaskTypesWithNull = array_unique(array_map(function ($taskTypeId) { + try { + return $this->taskProcessingManager->getPreferredProvider($taskTypeId)->getId(); + } catch (Exception) { + return null; + } + }, $taskTypeIds)); + + $providerIdsBasedOnTaskTypes = array_filter($providerIdsBasedOnTaskTypesWithNull, fn ($providerId) => $providerId !== null); + + // restrict $providerIds to providers that are configured as preferred for the passed task types + $possibleProviderIds = array_values(array_intersect($providerIdsBasedOnTaskTypes, $providerIds)); + + // restrict $taskTypeIds to task types that can actually be run by one of the now restricted providers + $possibleTaskTypeIds = array_values(array_filter($taskTypeIds, function ($taskTypeId) use ($possibleProviderIds) { + try { + $providerForTaskType = $this->taskProcessingManager->getPreferredProvider($taskTypeId)->getId(); + } catch (Exception) { + // no provider found for task type + return false; + } + return in_array($providerForTaskType, $possibleProviderIds, true); + })); + + if (count($possibleProviderIds) === 0 || count($possibleTaskTypeIds) === 0) { + throw new NotFoundException(); + } + + $taskIdsToIgnore = []; + while (true) { + // Until we find a task whose task type is set to be provided by the providers requested with this request + // Or no scheduled task is found anymore (given the taskIds to ignore) + $task = $this->taskProcessingManager->getNextScheduledTask($possibleTaskTypeIds, $taskIdsToIgnore); + try { + $provider = $this->taskProcessingManager->getPreferredProvider($task->getTaskTypeId()); + if (in_array($provider->getId(), $possibleProviderIds, true)) { + if ($this->taskProcessingManager->lockTask($task)) { + break; + } + } + } catch (Exception) { + // There is no provider set for the task type of this task + // proceed to ignore this task + } + + $taskIdsToIgnore[] = (int)$task->getId(); + } + + /** @var CoreTaskProcessingTask $json */ + $json = $task->jsonSerialize(); + + return new DataResponse([ + 'task' => $json, + 'provider' => [ + 'name' => $provider->getId(), + ], + ]); + } catch (NotFoundException) { + return new DataResponse(null, Http::STATUS_NO_CONTENT); + } catch (Exception) { + return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + /** + * @param resource $data + * @return int + * @throws NotPermittedException + */ + private function setFileContentsInternal($data): int { + try { + $folder = $this->appData->getFolder('TaskProcessing'); + } catch (\OCP\Files\NotFoundException) { + $folder = $this->appData->newFolder('TaskProcessing'); + } + /** @var SimpleFile $file */ + $file = $folder->newFile(time() . '-' . rand(1, 100000), $data); + return $file->getId(); + } +} diff --git a/core/Controller/TeamsApiController.php b/core/Controller/TeamsApiController.php index f937bcb847c..2eb33a0c254 100644 --- a/core/Controller/TeamsApiController.php +++ b/core/Controller/TeamsApiController.php @@ -2,33 +2,18 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2024 Julius Härtl <jus@bitgrid.net> - * - * @author Julius Härtl <jus@bitgrid.net> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; -use OCA\Core\ResponseDefinitions; +use OC\Core\ResponseDefinitions; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\ApiRoute; use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCSController; use OCP\IRequest; use OCP\Teams\ITeamManager; use OCP\Teams\Team; @@ -38,7 +23,7 @@ use OCP\Teams\Team; * @psalm-import-type CoreTeam from ResponseDefinitions * @property $userId string */ -class TeamsApiController extends \OCP\AppFramework\OCSController { +class TeamsApiController extends OCSController { public function __construct( string $appName, IRequest $request, @@ -52,7 +37,7 @@ class TeamsApiController extends \OCP\AppFramework\OCSController { * Get all resources of a team * * @param string $teamId Unique id of the team - * @return DataResponse<Http::STATUS_OK, array{resources: CoreTeamResource[]}, array{}> + * @return DataResponse<Http::STATUS_OK, array{resources: list<CoreTeamResource>}, array{}> * * 200: Resources returned */ @@ -60,7 +45,7 @@ class TeamsApiController extends \OCP\AppFramework\OCSController { #[ApiRoute(verb: 'GET', url: '/{teamId}/resources', root: '/teams')] public function resolveOne(string $teamId): DataResponse { /** - * @var CoreTeamResource[] $resolvedResources + * @var list<CoreTeamResource> $resolvedResources * @psalm-suppress PossiblyNullArgument The route is limited to logged-in users */ $resolvedResources = $this->teamManager->getSharedWith($teamId, $this->userId); @@ -73,7 +58,7 @@ class TeamsApiController extends \OCP\AppFramework\OCSController { * * @param string $providerId Identifier of the provider (e.g. deck, talk, collectives) * @param string $resourceId Unique id of the resource to list teams for (e.g. deck board id) - * @return DataResponse<Http::STATUS_OK, array{teams: CoreTeam[]}, array{}> + * @return DataResponse<Http::STATUS_OK, array{teams: list<CoreTeam>}, array{}> * * 200: Teams returned */ @@ -82,13 +67,13 @@ class TeamsApiController extends \OCP\AppFramework\OCSController { public function listTeams(string $providerId, string $resourceId): DataResponse { /** @psalm-suppress PossiblyNullArgument The route is limited to logged-in users */ $teams = $this->teamManager->getTeamsForResource($providerId, $resourceId, $this->userId); - /** @var CoreTeam[] $teams */ - $teams = array_map(function (Team $team) { + /** @var list<CoreTeam> $teams */ + $teams = array_values(array_map(function (Team $team) { $response = $team->jsonSerialize(); /** @psalm-suppress PossiblyNullArgument The route is limited to logged in users */ $response['resources'] = $this->teamManager->getSharedWith($team->getId(), $this->userId); return $response; - }, $teams); + }, $teams)); return new DataResponse([ 'teams' => $teams, diff --git a/core/Controller/TextProcessingApiController.php b/core/Controller/TextProcessingApiController.php index 6ba98f99f51..d3e6967f169 100644 --- a/core/Controller/TextProcessingApiController.php +++ b/core/Controller/TextProcessingApiController.php @@ -3,31 +3,15 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> - * - * @author Marcel Klehr <mklehr@gmx.net> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; use InvalidArgumentException; -use OCA\Core\ResponseDefinitions; +use OC\Core\ResponseDefinitions; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\AnonRateLimit; use OCP\AppFramework\Http\Attribute\ApiRoute; @@ -35,6 +19,7 @@ use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\Attribute\UserRateLimit; use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCSController; use OCP\Common\Exception\NotFoundException; use OCP\DB\Exception; use OCP\IL10N; @@ -52,15 +37,15 @@ use Psr\Log\LoggerInterface; /** * @psalm-import-type CoreTextProcessingTask from ResponseDefinitions */ -class TextProcessingApiController extends \OCP\AppFramework\OCSController { +class TextProcessingApiController extends OCSController { public function __construct( - string $appName, - IRequest $request, - private IManager $textProcessingManager, - private IL10N $l, - private ?string $userId, + string $appName, + IRequest $request, + private IManager $textProcessingManager, + private IL10N $l, + private ?string $userId, private ContainerInterface $container, - private LoggerInterface $logger, + private LoggerInterface $logger, ) { parent::__construct($appName, $request); } @@ -68,7 +53,7 @@ class TextProcessingApiController extends \OCP\AppFramework\OCSController { /** * This endpoint returns all available LanguageModel task types * - * @return DataResponse<Http::STATUS_OK, array{types: array{id: string, name: string, description: string}[]}, array{}> + * @return DataResponse<Http::STATUS_OK, array{types: list<array{id: string, name: string, description: string}>}, array{}> * * 200: Task types returned */ @@ -125,7 +110,7 @@ class TextProcessingApiController extends \OCP\AppFramework\OCSController { try { try { $this->textProcessingManager->runOrScheduleTask($task); - } catch(TaskFailureException) { + } catch (TaskFailureException) { // noop, because the task object has the failure status set already, we just return the task json } @@ -207,19 +192,18 @@ class TextProcessingApiController extends \OCP\AppFramework\OCSController { * * @param string $appId ID of the app * @param string|null $identifier An arbitrary identifier for the task - * @return DataResponse<Http::STATUS_OK, array{tasks: CoreTextProcessingTask[]}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}> + * @return DataResponse<Http::STATUS_OK, array{tasks: list<CoreTextProcessingTask>}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}> * - * 200: Task list returned + * 200: Task list returned */ #[NoAdminRequired] #[ApiRoute(verb: 'GET', url: '/tasks/app/{appId}', root: '/textprocessing')] public function listTasksByApp(string $appId, ?string $identifier = null): DataResponse { try { $tasks = $this->textProcessingManager->getUserTasksByApp($this->userId, $appId, $identifier); - /** @var CoreTextProcessingTask[] $json */ - $json = array_map(static function (Task $task) { + $json = array_values(array_map(static function (Task $task) { return $task->jsonSerialize(); - }, $tasks); + }, $tasks)); return new DataResponse([ 'tasks' => $json, diff --git a/core/Controller/TextToImageApiController.php b/core/Controller/TextToImageApiController.php index 8dd21e90664..d2c3e1ec288 100644 --- a/core/Controller/TextToImageApiController.php +++ b/core/Controller/TextToImageApiController.php @@ -3,31 +3,15 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> - * - * @author Marcel Klehr <mklehr@gmx.net> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; +use OC\Core\ResponseDefinitions; use OC\Files\AppData\AppData; -use OCA\Core\ResponseDefinitions; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\AnonRateLimit; use OCP\AppFramework\Http\Attribute\ApiRoute; @@ -37,6 +21,7 @@ use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\Attribute\UserRateLimit; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\FileDisplayResponse; +use OCP\AppFramework\OCSController; use OCP\DB\Exception; use OCP\Files\NotFoundException; use OCP\IL10N; @@ -50,7 +35,7 @@ use OCP\TextToImage\Task; /** * @psalm-import-type CoreTextToImageTask from ResponseDefinitions */ -class TextToImageApiController extends \OCP\AppFramework\OCSController { +class TextToImageApiController extends OCSController { public function __construct( string $appName, IRequest $request, @@ -166,12 +151,12 @@ class TextToImageApiController extends \OCP\AppFramework\OCSController { $task = $this->textToImageManager->getUserTask($id, $this->userId); try { $folder = $this->appData->getFolder('text2image'); - } catch(NotFoundException) { + } catch (NotFoundException) { $res = new DataResponse(['message' => $this->l->t('Image not found')], Http::STATUS_NOT_FOUND); $res->throttle(['action' => 'text2image']); return $res; } - $file = $folder->getFolder((string) $task->getId())->getFile((string) $index); + $file = $folder->getFolder((string)$task->getId())->getFile((string)$index); $info = getimagesizefromstring($file->getContent()); return new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => image_type_to_mime_type($info[2])]); @@ -228,9 +213,9 @@ class TextToImageApiController extends \OCP\AppFramework\OCSController { * * @param string $appId ID of the app * @param string|null $identifier An arbitrary identifier for the task - * @return DataResponse<Http::STATUS_OK, array{tasks: CoreTextToImageTask[]}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}> + * @return DataResponse<Http::STATUS_OK, array{tasks: list<CoreTextToImageTask>}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}> * - * 200: Task list returned + * 200: Task list returned */ #[NoAdminRequired] #[AnonRateLimit(limit: 5, period: 120)] @@ -238,10 +223,9 @@ class TextToImageApiController extends \OCP\AppFramework\OCSController { public function listTasksByApp(string $appId, ?string $identifier = null): DataResponse { try { $tasks = $this->textToImageManager->getUserTasksByApp($this->userId, $appId, $identifier); - /** @var CoreTextToImageTask[] $json */ - $json = array_map(static function (Task $task) { + $json = array_values(array_map(static function (Task $task) { return $task->jsonSerialize(); - }, $tasks); + }, $tasks)); return new DataResponse([ 'tasks' => $json, diff --git a/core/Controller/TranslationApiController.php b/core/Controller/TranslationApiController.php index 4cc0ec95ca1..73dd0657230 100644 --- a/core/Controller/TranslationApiController.php +++ b/core/Controller/TranslationApiController.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net> - * - * @author Julius Härtl <jus@bitgrid.net> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ @@ -29,15 +12,19 @@ namespace OC\Core\Controller; use InvalidArgumentException; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\AnonRateLimit; use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\PublicPage; +use OCP\AppFramework\Http\Attribute\UserRateLimit; use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCSController; use OCP\IL10N; use OCP\IRequest; use OCP\PreConditionNotMetException; use OCP\Translation\CouldNotTranslateException; use OCP\Translation\ITranslationManager; -class TranslationApiController extends \OCP\AppFramework\OCSController { +class TranslationApiController extends OCSController { public function __construct( string $appName, IRequest $request, @@ -48,27 +35,22 @@ class TranslationApiController extends \OCP\AppFramework\OCSController { } /** - * @PublicPage - * * Get the list of supported languages * - * @return DataResponse<Http::STATUS_OK, array{languages: array{from: string, fromLabel: string, to: string, toLabel: string}[], languageDetection: bool}, array{}> + * @return DataResponse<Http::STATUS_OK, array{languages: list<array{from: string, fromLabel: string, to: string, toLabel: string}>, languageDetection: bool}, array{}> * * 200: Supported languages returned */ + #[PublicPage] #[ApiRoute(verb: 'GET', url: '/languages', root: '/translation')] public function languages(): DataResponse { return new DataResponse([ - 'languages' => array_map(fn ($lang) => $lang->jsonSerialize(), $this->translationManager->getLanguages()), + 'languages' => array_values(array_map(fn ($lang) => $lang->jsonSerialize(), $this->translationManager->getLanguages())), 'languageDetection' => $this->translationManager->canDetectLanguage(), ]); } /** - * @PublicPage - * @UserRateThrottle(limit=25, period=120) - * @AnonRateThrottle(limit=10, period=120) - * * Translate a text * * @param string $text Text to be translated @@ -80,6 +62,9 @@ class TranslationApiController extends \OCP\AppFramework\OCSController { * 400: Language not detected or unable to translate * 412: Translating is not possible */ + #[PublicPage] + #[UserRateLimit(limit: 25, period: 120)] + #[AnonRateLimit(limit: 10, period: 120)] #[ApiRoute(verb: 'POST', url: '/translate', root: '/translation')] public function translate(string $text, ?string $fromLanguage, string $toLanguage): DataResponse { try { diff --git a/core/Controller/TwoFactorApiController.php b/core/Controller/TwoFactorApiController.php new file mode 100644 index 00000000000..8d89963e6ad --- /dev/null +++ b/core/Controller/TwoFactorApiController.php @@ -0,0 +1,99 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Core\Controller; + +use OC\Authentication\TwoFactorAuth\ProviderManager; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCSController; +use OCP\Authentication\TwoFactorAuth\IRegistry; +use OCP\IRequest; +use OCP\IUserManager; + +class TwoFactorApiController extends OCSController { + public function __construct( + string $appName, + IRequest $request, + private ProviderManager $tfManager, + private IRegistry $tfRegistry, + private IUserManager $userManager, + ) { + parent::__construct($appName, $request); + } + + /** + * Get two factor authentication provider states + * + * @param string $user system user id + * + * @return DataResponse<Http::STATUS_OK, array<string, bool>, array{}>|DataResponse<Http::STATUS_NOT_FOUND, null, array{}> + * + * 200: provider states + * 404: user not found + */ + #[ApiRoute(verb: 'GET', url: '/state', root: '/twofactor')] + public function state(string $user): DataResponse { + $userObject = $this->userManager->get($user); + if ($userObject !== null) { + $state = $this->tfRegistry->getProviderStates($userObject); + return new DataResponse($state); + } + return new DataResponse(null, Http::STATUS_NOT_FOUND); + } + + /** + * Enable two factor authentication providers for specific user + * + * @param string $user system user identifier + * @param list<string> $providers collection of TFA provider ids + * + * @return DataResponse<Http::STATUS_OK, array<string, bool>, array{}>|DataResponse<Http::STATUS_NOT_FOUND, null, array{}> + * + * 200: provider states + * 404: user not found + */ + #[ApiRoute(verb: 'POST', url: '/enable', root: '/twofactor')] + public function enable(string $user, array $providers = []): DataResponse { + $userObject = $this->userManager->get($user); + if ($userObject !== null) { + foreach ($providers as $providerId) { + $this->tfManager->tryEnableProviderFor($providerId, $userObject); + } + $state = $this->tfRegistry->getProviderStates($userObject); + return new DataResponse($state); + } + return new DataResponse(null, Http::STATUS_NOT_FOUND); + } + + /** + * Disable two factor authentication providers for specific user + * + * @param string $user system user identifier + * @param list<string> $providers collection of TFA provider ids + * + * @return DataResponse<Http::STATUS_OK, array<string, bool>, array{}>|DataResponse<Http::STATUS_NOT_FOUND, null, array{}> + * + * 200: provider states + * 404: user not found + */ + #[ApiRoute(verb: 'POST', url: '/disable', root: '/twofactor')] + public function disable(string $user, array $providers = []): DataResponse { + $userObject = $this->userManager->get($user); + if ($userObject !== null) { + foreach ($providers as $providerId) { + $this->tfManager->tryDisableProviderFor($providerId, $userObject); + } + $state = $this->tfRegistry->getProviderStates($userObject); + return new DataResponse($state); + } + return new DataResponse(null, Http::STATUS_NOT_FOUND); + } + +} diff --git a/core/Controller/TwoFactorChallengeController.php b/core/Controller/TwoFactorChallengeController.php index 48fd365fd12..4791139bb12 100644 --- a/core/Controller/TwoFactorChallengeController.php +++ b/core/Controller/TwoFactorChallengeController.php @@ -1,28 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Cornelius Kölbel <cornelius.koelbel@netknights.it> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Controller; @@ -30,6 +11,8 @@ use OC\Authentication\TwoFactorAuth\Manager; use OC_User; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\Attribute\OpenAPI; use OCP\AppFramework\Http\Attribute\UseSession; use OCP\AppFramework\Http\RedirectResponse; @@ -42,6 +25,7 @@ use OCP\IRequest; use OCP\ISession; use OCP\IURLGenerator; use OCP\IUserSession; +use OCP\Util; use Psr\Log\LoggerInterface; #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] @@ -83,13 +67,13 @@ class TwoFactorChallengeController extends Controller { } /** - * @NoAdminRequired - * @NoCSRFRequired * @TwoFactorSetUpDoneRequired * * @param string $redirect_url * @return StandaloneTemplateResponse */ + #[NoAdminRequired] + #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: '/login/selectchallenge')] public function selectChallenge($redirect_url) { $user = $this->userSession->getUser(); @@ -106,18 +90,19 @@ class TwoFactorChallengeController extends Controller { 'logout_url' => $this->getLogoutUrl(), 'hasSetupProviders' => !empty($setupProviders), ]; + Util::addScript('core', 'twofactor-request-token'); return new StandaloneTemplateResponse($this->appName, 'twofactorselectchallenge', $data, 'guest'); } /** - * @NoAdminRequired - * @NoCSRFRequired * @TwoFactorSetUpDoneRequired * * @param string $challengeProviderId * @param string $redirect_url * @return StandaloneTemplateResponse|RedirectResponse */ + #[NoAdminRequired] + #[NoCSRFRequired] #[UseSession] #[FrontpageRoute(verb: 'GET', url: '/login/challenge/{challengeProviderId}')] public function showChallenge($challengeProviderId, $redirect_url) { @@ -140,7 +125,7 @@ class TwoFactorChallengeController extends Controller { if ($this->session->exists('two_factor_auth_error')) { $this->session->remove('two_factor_auth_error'); $error = true; - $errorMessage = $this->session->get("two_factor_auth_error_message"); + $errorMessage = $this->session->get('two_factor_auth_error_message'); $this->session->remove('two_factor_auth_error_message'); } $tmpl = $provider->getTemplate($user); @@ -158,12 +143,11 @@ class TwoFactorChallengeController extends Controller { if ($provider instanceof IProvidesCustomCSP) { $response->setContentSecurityPolicy($provider->getCSP()); } + Util::addScript('core', 'twofactor-request-token'); return $response; } /** - * @NoAdminRequired - * @NoCSRFRequired * @TwoFactorSetUpDoneRequired * * @UserRateThrottle(limit=5, period=100) @@ -173,6 +157,8 @@ class TwoFactorChallengeController extends Controller { * @param string $redirect_url * @return RedirectResponse */ + #[NoAdminRequired] + #[NoCSRFRequired] #[UseSession] #[FrontpageRoute(verb: 'POST', url: '/login/challenge/{challengeProviderId}')] public function solveChallenge($challengeProviderId, $challenge, $redirect_url = null) { @@ -208,10 +194,8 @@ class TwoFactorChallengeController extends Controller { ])); } - /** - * @NoAdminRequired - * @NoCSRFRequired - */ + #[NoAdminRequired] + #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: 'login/setupchallenge')] public function setupProviders(?string $redirect_url = null): StandaloneTemplateResponse { $user = $this->userSession->getUser(); @@ -223,13 +207,12 @@ class TwoFactorChallengeController extends Controller { 'redirect_url' => $redirect_url, ]; + Util::addScript('core', 'twofactor-request-token'); return new StandaloneTemplateResponse($this->appName, 'twofactorsetupselection', $data, 'guest'); } - /** - * @NoAdminRequired - * @NoCSRFRequired - */ + #[NoAdminRequired] + #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: 'login/setupchallenge/{providerId}')] public function setupProvider(string $providerId, ?string $redirect_url = null) { $user = $this->userSession->getUser(); @@ -256,15 +239,15 @@ class TwoFactorChallengeController extends Controller { 'template' => $tmpl->fetchPage(), ]; $response = new StandaloneTemplateResponse($this->appName, 'twofactorsetupchallenge', $data, 'guest'); + Util::addScript('core', 'twofactor-request-token'); return $response; } /** - * @NoAdminRequired - * @NoCSRFRequired - * * @todo handle the extreme edge case of an invalid provider ID and redirect to the provider selection page */ + #[NoAdminRequired] + #[NoCSRFRequired] #[FrontpageRoute(verb: 'POST', url: 'login/setupchallenge/{providerId}')] public function confirmProviderSetup(string $providerId, ?string $redirect_url = null) { return new RedirectResponse($this->urlGenerator->linkToRoute( diff --git a/core/Controller/UnifiedSearchController.php b/core/Controller/UnifiedSearchController.php index 469c6c6ed7b..c770c6240df 100644 --- a/core/Controller/UnifiedSearchController.php +++ b/core/Controller/UnifiedSearchController.php @@ -3,38 +3,20 @@ declare(strict_types=1); /** - * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; use InvalidArgumentException; +use OC\Core\ResponseDefinitions; use OC\Search\SearchComposer; use OC\Search\SearchQuery; use OC\Search\UnsupportedFilter; -use OCA\Core\ResponseDefinitions; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; use OCP\IRequest; @@ -60,16 +42,15 @@ class UnifiedSearchController extends OCSController { } /** - * @NoAdminRequired - * @NoCSRFRequired - * * Get the providers for unified search * * @param string $from the url the user is currently at - * @return DataResponse<Http::STATUS_OK, CoreUnifiedSearchProvider[], array{}> + * @return DataResponse<Http::STATUS_OK, list<CoreUnifiedSearchProvider>, array{}> * * 200: Providers returned */ + #[NoAdminRequired] + #[NoCSRFRequired] #[ApiRoute(verb: 'GET', url: '/providers', root: '/search')] public function getProviders(string $from = ''): DataResponse { [$route, $parameters] = $this->getRouteInformation($from); @@ -81,9 +62,6 @@ class UnifiedSearchController extends OCSController { } /** - * @NoAdminRequired - * @NoCSRFRequired - * * Launch a search for a specific search provider. * * Additional filters are available for each provider. @@ -92,7 +70,7 @@ class UnifiedSearchController extends OCSController { * @param string $providerId ID of the provider * @param string $term Term to search * @param int|null $sortOrder Order of entries - * @param int|null $limit Maximum amount of entries + * @param int|null $limit Maximum amount of entries, limited to 25 * @param int|string|null $cursor Offset for searching * @param string $from The current user URL * @@ -101,6 +79,8 @@ class UnifiedSearchController extends OCSController { * 200: Search entries returned * 400: Searching is not possible */ + #[NoAdminRequired] + #[NoCSRFRequired] #[ApiRoute(verb: 'GET', url: '/providers/{providerId}/search', root: '/search')] public function search( string $providerId, @@ -113,6 +93,9 @@ class UnifiedSearchController extends OCSController { ): DataResponse { [$route, $routeParameters] = $this->getRouteInformation($from); + $limit ??= SearchQuery::LIMIT_DEFAULT; + $limit = max(1, min($limit, 25)); + try { $filters = $this->composer->buildFilterList($providerId, $this->request->getParams()); } catch (UnsupportedFilter|InvalidArgumentException $e) { @@ -125,7 +108,7 @@ class UnifiedSearchController extends OCSController { new SearchQuery( $filters, $sortOrder ?? ISearchQuery::SORT_DATE_DESC, - $limit ?? SearchQuery::LIMIT_DEFAULT, + $limit, $cursor, $route, $routeParameters diff --git a/core/Controller/UnsupportedBrowserController.php b/core/Controller/UnsupportedBrowserController.php index dfcff8df381..2877e2e9047 100644 --- a/core/Controller/UnsupportedBrowserController.php +++ b/core/Controller/UnsupportedBrowserController.php @@ -3,33 +3,17 @@ declare(strict_types=1); /** - * @copyright 2021 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\TemplateResponse; use OCP\IRequest; @@ -42,15 +26,16 @@ class UnsupportedBrowserController extends Controller { } /** - * @PublicPage - * @NoCSRFRequired - * * @return Response */ + #[PublicPage] + #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: 'unsupported')] public function index(): Response { Util::addScript('core', 'unsupported-browser'); Util::addStyle('core', 'icons'); - return new TemplateResponse('core', 'unsupportedbrowser', [], TemplateResponse::RENDER_AS_ERROR); + + // not using RENDER_AS_ERROR as we need the JSConfigHelper for url generation + return new TemplateResponse('core', 'unsupportedbrowser', [], TemplateResponse::RENDER_AS_GUEST); } } diff --git a/core/Controller/UserController.php b/core/Controller/UserController.php index c941a80e53d..b6e464d9a95 100644 --- a/core/Controller/UserController.php +++ b/core/Controller/UserController.php @@ -1,31 +1,15 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Controller; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\JSONResponse; use OCP\IRequest; use OCP\IUserManager; @@ -42,12 +26,11 @@ class UserController extends Controller { /** * Lookup user display names * - * @NoAdminRequired - * * @param array $users * * @return JSONResponse */ + #[NoAdminRequired] #[FrontpageRoute(verb: 'POST', url: '/displaynames')] public function getDisplayNames($users) { $result = []; diff --git a/core/Controller/WalledGardenController.php b/core/Controller/WalledGardenController.php index e5d8edd9083..d0bc0665534 100644 --- a/core/Controller/WalledGardenController.php +++ b/core/Controller/WalledGardenController.php @@ -1,41 +1,23 @@ <?php + /** - * @copyright 2017, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\Response; #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] class WalledGardenController extends Controller { - /** - * @PublicPage - * @NoCSRFRequired - */ + #[PublicPage] + #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: '/204')] public function get(): Response { $resp = new Response(); diff --git a/core/Controller/WebAuthnController.php b/core/Controller/WebAuthnController.php index 70034f08fcc..d7255831e88 100644 --- a/core/Controller/WebAuthnController.php +++ b/core/Controller/WebAuthnController.php @@ -3,27 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Richard Steinmetz <richard@steinmetz.cloud> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; @@ -34,6 +15,7 @@ use OC\URLGenerator; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\Attribute\UseSession; use OCP\AppFramework\Http\JSONResponse; use OCP\IRequest; @@ -58,10 +40,7 @@ class WebAuthnController extends Controller { parent::__construct($appName, $request); } - /** - * @NoAdminRequired - * @PublicPage - */ + #[PublicPage] #[UseSession] #[FrontpageRoute(verb: 'POST', url: 'login/webauthn/start')] public function startAuthentication(string $loginName): JSONResponse { @@ -83,10 +62,7 @@ class WebAuthnController extends Controller { return new JSONResponse($publicKeyCredentialRequestOptions); } - /** - * @NoAdminRequired - * @PublicPage - */ + #[PublicPage] #[UseSession] #[FrontpageRoute(verb: 'POST', url: 'login/webauthn/finish')] public function finishAuthentication(string $data): JSONResponse { diff --git a/core/Controller/WellKnownController.php b/core/Controller/WellKnownController.php index 0e6b7ee3ef8..9ce83686355 100644 --- a/core/Controller/WellKnownController.php +++ b/core/Controller/WellKnownController.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; @@ -30,7 +12,9 @@ use OC\Http\WellKnown\RequestManager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\Response; use OCP\IRequest; @@ -45,11 +29,10 @@ class WellKnownController extends Controller { } /** - * @PublicPage - * @NoCSRFRequired - * * @return Response */ + #[PublicPage] + #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: '.well-known/{service}')] public function handle(string $service): Response { $response = $this->requestManager->process( @@ -58,7 +41,7 @@ class WellKnownController extends Controller { ); if ($response === null) { - $httpResponse = new JSONResponse(["message" => "$service not supported"], Http::STATUS_NOT_FOUND); + $httpResponse = new JSONResponse(['message' => "$service not supported"], Http::STATUS_NOT_FOUND); } else { $httpResponse = $response->toHttpResponse(); } diff --git a/core/Controller/WhatsNewController.php b/core/Controller/WhatsNewController.php index ab107cd115d..af8c3d4853b 100644 --- a/core/Controller/WhatsNewController.php +++ b/core/Controller/WhatsNewController.php @@ -1,26 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2018 Arthur Schiwon <blizzz@arthur-schiwon.de> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; @@ -30,6 +12,7 @@ use OC\Updater\ChangesCheck; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\DataResponse; use OCP\Defaults; use OCP\IConfig; @@ -37,6 +20,8 @@ use OCP\IRequest; use OCP\IUserManager; use OCP\IUserSession; use OCP\L10N\IFactory; +use OCP\PreConditionNotMetException; +use OCP\ServerVersion; class WhatsNewController extends OCSController { public function __construct( @@ -46,29 +31,29 @@ class WhatsNewController extends OCSController { private IUserSession $userSession, IUserManager $userManager, Manager $keyManager, + ServerVersion $serverVersion, private IConfig $config, private ChangesCheck $whatsNewService, private IFactory $langFactory, private Defaults $defaults, ) { - parent::__construct($appName, $request, $capabilitiesManager, $userSession, $userManager, $keyManager); + parent::__construct($appName, $request, $capabilitiesManager, $userSession, $userManager, $keyManager, $serverVersion); } /** - * @NoAdminRequired - * * Get the changes * - * @return DataResponse<Http::STATUS_OK, array{changelogURL: string, product: string, version: string, whatsNew?: array{regular: string[], admin: string[]}}, array{}>|DataResponse<Http::STATUS_NO_CONTENT, array<empty>, array{}> + * @return DataResponse<Http::STATUS_OK, array{changelogURL: string, product: string, version: string, whatsNew?: array{regular: list<string>, admin: list<string>}}, array{}>|DataResponse<Http::STATUS_NO_CONTENT, list<empty>, array{}> * * 200: Changes returned * 204: No changes */ + #[NoAdminRequired] #[ApiRoute(verb: 'GET', url: '/whatsnew', root: '/core')] public function get():DataResponse { $user = $this->userSession->getUser(); if ($user === null) { - throw new \RuntimeException("Acting user cannot be resolved"); + throw new \RuntimeException('Acting user cannot be resolved'); } $lastRead = $this->config->getUserValue($user->getUID(), 'core', 'whatsNewLastRead', 0); $currentVersion = $this->whatsNewService->normalizeVersion($this->config->getSystemValue('version')); @@ -100,23 +85,22 @@ class WhatsNewController extends OCSController { } /** - * @NoAdminRequired - * * Dismiss the changes * * @param string $version Version to dismiss the changes for * - * @return DataResponse<Http::STATUS_OK, array<empty>, array{}> - * @throws \OCP\PreConditionNotMetException + * @return DataResponse<Http::STATUS_OK, list<empty>, array{}> + * @throws PreConditionNotMetException * @throws DoesNotExistException * * 200: Changes dismissed */ + #[NoAdminRequired] #[ApiRoute(verb: 'POST', url: '/whatsnew', root: '/core')] public function dismiss(string $version):DataResponse { $user = $this->userSession->getUser(); if ($user === null) { - throw new \RuntimeException("Acting user cannot be resolved"); + throw new \RuntimeException('Acting user cannot be resolved'); } $version = $this->whatsNewService->normalizeVersion($version); // checks whether it's a valid version, throws an Exception otherwise diff --git a/core/Controller/WipeController.php b/core/Controller/WipeController.php index 3e486003668..1b57be71aa0 100644 --- a/core/Controller/WipeController.php +++ b/core/Controller/WipeController.php @@ -3,37 +3,24 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Controller; use OC\Authentication\Token\RemoteWipe; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\AnonRateLimit; use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\JSONResponse; use OCP\Authentication\Exceptions\InvalidTokenException; use OCP\IRequest; +#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] class WipeController extends Controller { public function __construct( string $appName, @@ -44,21 +31,18 @@ class WipeController extends Controller { } /** - * @NoAdminRequired - * @NoCSRFRequired - * @PublicPage - * - * @AnonRateThrottle(limit=10, period=300) - * * Check if the device should be wiped * * @param string $token App password * - * @return JSONResponse<Http::STATUS_OK, array{wipe: bool}, array{}>|JSONResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}> + * @return JSONResponse<Http::STATUS_OK, array{wipe: bool}, array{}>|JSONResponse<Http::STATUS_NOT_FOUND, list<empty>, array{}> * * 200: Device should be wiped * 404: Device should not be wiped */ + #[PublicPage] + #[NoCSRFRequired] + #[AnonRateLimit(limit: 10, period: 300)] #[FrontpageRoute(verb: 'POST', url: '/core/wipe/check')] public function checkWipe(string $token): JSONResponse { try { @@ -76,21 +60,18 @@ class WipeController extends Controller { /** - * @NoAdminRequired - * @NoCSRFRequired - * @PublicPage - * - * @AnonRateThrottle(limit=10, period=300) - * * Finish the wipe * * @param string $token App password * - * @return JSONResponse<Http::STATUS_OK|Http::STATUS_NOT_FOUND, array<empty>, array{}> + * @return JSONResponse<Http::STATUS_OK|Http::STATUS_NOT_FOUND, list<empty>, array{}> * * 200: Wipe finished successfully * 404: Device should not be wiped */ + #[PublicPage] + #[NoCSRFRequired] + #[AnonRateLimit(limit: 10, period: 300)] #[FrontpageRoute(verb: 'POST', url: '/core/wipe/success')] public function wipeDone(string $token): JSONResponse { try { |