diff options
Diffstat (limited to 'core/Controller')
30 files changed, 474 insertions, 113 deletions
diff --git a/core/Controller/AppPasswordController.php b/core/Controller/AppPasswordController.php index 16ec124e23a..e5edc165bf5 100644 --- a/core/Controller/AppPasswordController.php +++ b/core/Controller/AppPasswordController.php @@ -20,6 +20,7 @@ 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; @@ -31,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, @@ -76,7 +77,7 @@ 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); diff --git a/core/Controller/AvatarController.php b/core/Controller/AvatarController.php index 4d5e810ddb9..b577b2fd460 100644 --- a/core/Controller/AvatarController.php +++ b/core/Controller/AvatarController.php @@ -8,11 +8,13 @@ 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; @@ -20,9 +22,11 @@ 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; @@ -66,6 +70,7 @@ class AvatarController extends Controller { #[NoCSRFRequired] #[PublicPage] #[FrontpageRoute(verb: 'GET', url: '/avatar/{userId}/{size}/dark')] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] public function getAvatarDark(string $userId, int $size, bool $guestFallback = false) { if ($size <= 64) { if ($size !== 64) { @@ -117,6 +122,7 @@ class AvatarController extends Controller { #[NoCSRFRequired] #[PublicPage] #[FrontpageRoute(verb: 'GET', url: '/avatar/{userId}/{size}')] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] public function getAvatar(string $userId, int $size, bool $guestFallback = false) { if ($size <= 64) { if ($size !== 64) { @@ -179,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 @@ -187,8 +193,8 @@ class AvatarController extends Controller { } } elseif (!is_null($files)) { if ( - $files['error'][0] === 0 && - is_uploaded_file($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( @@ -226,7 +232,7 @@ class AvatarController extends Controller { } try { - $image = new \OCP\Image(); + $image = new Image(); $image->loadFromData($content); $image->readExif($content); $image->fixOrientation(); @@ -297,7 +303,7 @@ class AvatarController extends Controller { Http::STATUS_NOT_FOUND); } - $image = new \OCP\Image(); + $image = new Image(); $image->loadFromData($tmpAvatar); $resp = new DataDisplayResponse( @@ -332,7 +338,7 @@ class AvatarController extends Controller { 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 { @@ -341,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 8ea475941c8..edf7c26e94c 100644 --- a/core/Controller/CSRFTokenController.php +++ b/core/Controller/CSRFTokenController.php @@ -13,6 +13,7 @@ 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; @@ -33,10 +34,13 @@ class CSRFTokenController extends Controller { * * 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 93eec8921fe..4464af890c4 100644 --- a/core/Controller/ClientFlowLoginController.php +++ b/core/Controller/ClientFlowLoginController.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -8,7 +9,6 @@ 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; @@ -18,14 +18,19 @@ 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; @@ -55,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'; } @@ -89,7 +95,7 @@ class ClientFlowLoginController extends Controller { #[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 !== '') { @@ -104,8 +110,8 @@ class ClientFlowLoginController extends Controller { $this->appName, 'error', [ - 'errors' => - [ + 'errors' + => [ [ 'error' => 'Access Forbidden', 'hint' => 'Invalid request', @@ -122,7 +128,7 @@ class ClientFlowLoginController extends Controller { ); $this->session->set(self::STATE_NAME, $stateToken); - $csp = new Http\ContentSecurityPolicy(); + $csp = new ContentSecurityPolicy(); if ($client) { $csp->addAllowedFormActionDomain($client->getRedirectUri()); } else { @@ -142,6 +148,7 @@ class ClientFlowLoginController extends Controller { 'oauthState' => $this->session->get('oauth.state'), 'user' => $user, 'direct' => $direct, + 'providedRedirectUri' => $providedRedirectUri, ], 'guest' ); @@ -157,9 +164,12 @@ class ClientFlowLoginController extends Controller { #[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(); } @@ -171,7 +181,7 @@ class ClientFlowLoginController extends Controller { $clientName = $client->getName(); } - $csp = new Http\ContentSecurityPolicy(); + $csp = new ContentSecurityPolicy(); if ($client) { $csp->addAllowedFormActionDomain($client->getRedirectUri()); } else { @@ -195,6 +205,7 @@ class ClientFlowLoginController extends Controller { 'serverHost' => $this->getServerPath(), 'oauthState' => $this->session->get('oauth.state'), 'direct' => $direct, + 'providedRedirectUri' => $providedRedirectUri, ], 'guest' ); @@ -203,14 +214,15 @@ class ClientFlowLoginController extends Controller { return $response; } - /** - * @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(); @@ -269,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 .= '&'; @@ -294,7 +318,7 @@ class ClientFlowLoginController extends Controller { new AppPasswordCreatedEvent($generatedToken) ); - return new Http\RedirectResponse($redirectUri); + return new RedirectResponse($redirectUri); } #[PublicPage] @@ -323,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 { diff --git a/core/Controller/ClientFlowLoginV2Controller.php b/core/Controller/ClientFlowLoginV2Controller.php index b973a57924e..8c0c1e8179d 100644 --- a/core/Controller/ClientFlowLoginV2Controller.php +++ b/core/Controller/ClientFlowLoginV2Controller.php @@ -9,6 +9,7 @@ declare(strict_types=1); 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; @@ -18,6 +19,7 @@ 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; @@ -33,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 @@ -41,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, @@ -69,6 +74,7 @@ class ClientFlowLoginV2Controller extends Controller { #[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); @@ -106,6 +112,8 @@ class ClientFlowLoginV2Controller extends Controller { $flow = $this->getFlowByLoginToken(); } catch (LoginFlowV2NotFoundException $e) { return $this->loginTokenForbiddenResponse(); + } catch (LoginFlowV2ClientForbiddenException $e) { + return $this->loginTokenForbiddenClientResponse(); } $stateToken = $this->random->generate( @@ -149,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 */ @@ -185,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); @@ -194,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'); } @@ -217,6 +229,7 @@ class ClientFlowLoginV2Controller extends Controller { #[NoAdminRequired] #[UseSession] + #[PasswordConfirmationRequired(strict: false)] #[FrontpageRoute(verb: 'POST', url: '/login/v2/grant')] public function generateAppPassword(?string $stateToken): Response { if ($stateToken === null) { @@ -230,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); @@ -275,9 +290,10 @@ class ClientFlowLoginV2Controller extends Controller { #[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); @@ -329,6 +345,7 @@ class ClientFlowLoginV2Controller extends Controller { /** * @return LoginFlowV2 * @throws LoginFlowV2NotFoundException + * @throws LoginFlowV2ClientForbiddenException */ private function getFlowByLoginToken(): LoginFlowV2 { $currentToken = $this->session->get(self::TOKEN_NAME); @@ -352,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/ContactsMenuController.php b/core/Controller/ContactsMenuController.php index f4ded1ed42b..d90ee8a1c61 100644 --- a/core/Controller/ContactsMenuController.php +++ b/core/Controller/ContactsMenuController.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/core/Controller/ErrorController.php b/core/Controller/ErrorController.php index 55925ffc941..d80dc3f76eb 100644 --- a/core/Controller/ErrorController.php +++ b/core/Controller/ErrorController.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace OC\Core\Controller; +use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\FrontpageRoute; use OCP\AppFramework\Http\Attribute\NoCSRFRequired; @@ -17,7 +18,7 @@ use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\TemplateResponse; #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] -class ErrorController extends \OCP\AppFramework\Controller { +class ErrorController extends Controller { #[PublicPage] #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: 'error/403')] diff --git a/core/Controller/GuestAvatarController.php b/core/Controller/GuestAvatarController.php index e87112726f2..711158e0708 100644 --- a/core/Controller/GuestAvatarController.php +++ b/core/Controller/GuestAvatarController.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -9,6 +10,7 @@ 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; @@ -46,6 +48,7 @@ class GuestAvatarController extends Controller { #[PublicPage] #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: '/avatar/guest/{guestName}/{size}')] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] public function getAvatar(string $guestName, int $size, ?bool $darkTheme = false) { $darkTheme = $darkTheme ?? false; @@ -74,7 +77,7 @@ class GuestAvatarController extends Controller { $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; } @@ -97,6 +100,7 @@ class GuestAvatarController extends Controller { #[PublicPage] #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: '/avatar/guest/{guestName}/{size}/dark')] + #[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 7a816e21d14..236a81760ac 100644 --- a/core/Controller/HoverCardController.php +++ b/core/Controller/HoverCardController.php @@ -13,6 +13,7 @@ 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; @@ -20,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, diff --git a/core/Controller/LoginController.php b/core/Controller/LoginController.php index 19d5aae9613..5a21d27898f 100644 --- a/core/Controller/LoginController.php +++ b/core/Controller/LoginController.php @@ -29,6 +29,7 @@ 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; @@ -42,6 +43,7 @@ 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 { @@ -91,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"'); } @@ -111,7 +113,7 @@ class LoginController extends Controller { #[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()); } @@ -224,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) { @@ -410,6 +412,7 @@ class LoginController extends Controller { #[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 001ab737c7e..d956f3427f2 100644 --- a/core/Controller/LostController.php +++ b/core/Controller/LostController.php @@ -14,6 +14,7 @@ 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; @@ -36,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; @@ -52,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, @@ -62,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, @@ -73,7 +75,6 @@ class LostController extends Controller { private Limiter $limiter, ) { parent::__construct($appName, $request); - $this->from = $defaultMailAddress; } /** @@ -158,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] @@ -217,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(); @@ -233,7 +234,7 @@ class LostController extends Controller { /** * @throws ResetPasswordException - * @throws \OCP\PreConditionNotMetException + * @throws PreConditionNotMetException */ protected function sendEmail(string $input): void { $user = $this->findUserByIdOrMail($input); @@ -280,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 de72e412945..017061ef979 100644 --- a/core/Controller/NavigationController.php +++ b/core/Controller/NavigationController.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -47,12 +48,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; } @@ -74,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 176558b013d..ea372b43b2e 100644 --- a/core/Controller/OCJSController.php +++ b/core/Controller/OCJSController.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/core/Controller/OCMController.php b/core/Controller/OCMController.php index f15a4a56779..2d3b99f431d 100644 --- a/core/Controller/OCMController.php +++ b/core/Controller/OCMController.php @@ -10,10 +10,12 @@ declare(strict_types=1); 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; @@ -51,12 +53,13 @@ class OCMController extends Controller { #[PublicPage] #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: '/ocm-provider/')] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] public function discovery(): DataResponse { try { $cap = Server::get( $this->appConfig->getValueString( 'core', 'ocm_providers', - \OCA\CloudFederationAPI\Capabilities::class, + Capabilities::class, lazy: true ) ); diff --git a/core/Controller/OCSController.php b/core/Controller/OCSController.php index 65ce55b8606..fb0280479c4 100644 --- a/core/Controller/OCSController.php +++ b/core/Controller/OCSController.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -17,6 +18,7 @@ use OCP\IRequest; use OCP\IUserManager; use OCP\IUserSession; use OCP\ServerVersion; +use OCP\Util; class OCSController extends \OCP\AppFramework\OCSController { public function __construct( @@ -63,7 +65,7 @@ class OCSController extends \OCP\AppFramework\OCSController { 'micro' => $this->serverVersion->getPatchVersion(), 'string' => $this->serverVersion->getVersionString(), 'edition' => '', - 'extendedSupport' => \OCP\Util::hasExtendedSupport() + 'extendedSupport' => Util::hasExtendedSupport() ]; if ($this->userSession->isLoggedIn()) { diff --git a/core/Controller/PreviewController.php b/core/Controller/PreviewController.php index 2720da671be..aac49c06d57 100644 --- a/core/Controller/PreviewController.php +++ b/core/Controller/PreviewController.php @@ -13,9 +13,12 @@ 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; @@ -58,6 +61,7 @@ class PreviewController extends Controller { #[NoAdminRequired] #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: '/core/preview.png')] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] public function getPreview( string $file = '', int $x = 32, @@ -65,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); } @@ -101,6 +105,7 @@ class PreviewController extends Controller { #[NoAdminRequired] #[NoCSRFRequired] #[FrontpageRoute(verb: 'GET', url: '/core/preview')] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] public function getPreviewByFileId( int $fileId = -1, int $x = 32, @@ -133,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); } @@ -147,15 +152,12 @@ class PreviewController extends Controller { // 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'; + $isNextcloudPreview = $this->request->getHeader('x-nc-preview') === 'true'; $storage = $node->getStorage(); if ($isNextcloudPreview === false && $storage->instanceOfStorage(ISharedStorage::class)) { /** @var ISharedStorage $storage */ $share = $storage->getShare(); - $attributes = $share->getAttributes(); - // No "allow preview" header set, so we must check if - // the share has not explicitly disabled download permissions - if ($attributes?->getAttribute('permissions', 'download') === false) { + if (!$share->canSeeContent()) { return new DataResponse([], Http::STATUS_FORBIDDEN); } } @@ -180,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 c807ecb72d4..02979cb1649 100644 --- a/core/Controller/ProfileApiController.php +++ b/core/Controller/ProfileApiController.php @@ -10,9 +10,11 @@ declare(strict_types=1); 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; @@ -21,17 +23,27 @@ 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); } @@ -57,14 +69,13 @@ class ProfileApiController extends OCSController { #[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 @@ -80,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/ReferenceApiController.php b/core/Controller/ReferenceApiController.php index 099fdb97194..d4fb753f404 100644 --- a/core/Controller/ReferenceApiController.php +++ b/core/Controller/ReferenceApiController.php @@ -15,6 +15,7 @@ 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; @@ -24,7 +25,7 @@ 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( diff --git a/core/Controller/ReferenceController.php b/core/Controller/ReferenceController.php index b4c88562bc9..6ed15e2d2f1 100644 --- a/core/Controller/ReferenceController.php +++ b/core/Controller/ReferenceController.php @@ -12,6 +12,7 @@ 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; @@ -43,6 +44,7 @@ class ReferenceController extends Controller { #[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/SetupController.php b/core/Controller/SetupController.php index eb78d74dd4e..f89506680ad 100644 --- a/core/Controller/SetupController.php +++ b/core/Controller/SetupController.php @@ -7,7 +7,12 @@ */ 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; @@ -17,6 +22,9 @@ 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'; } @@ -57,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' => '', @@ -70,6 +78,8 @@ class SetupController { 'dbtablespace' => '', 'dbhost' => 'localhost', 'dbtype' => '', + 'hasAutoconfig' => false, + 'serverRoot' => \OC::$SERVERROOT, ]; $parameters = array_merge($defaults, $post); @@ -78,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); } @@ -112,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 index 925d4751383..e60c9ebc789 100644 --- a/core/Controller/TaskProcessingApiController.php +++ b/core/Controller/TaskProcessingApiController.php @@ -17,13 +17,15 @@ 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\DataDownloadResponse; use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\StreamResponse; +use OCP\AppFramework\OCSController; use OCP\Files\File; -use OCP\Files\GenericFileException; use OCP\Files\IAppData; +use OCP\Files\IMimeTypeDetector; use OCP\Files\IRootFolder; use OCP\Files\NotPermittedException; use OCP\IL10N; @@ -39,12 +41,13 @@ 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 \OCP\AppFramework\OCSController { +class TaskProcessingApiController extends OCSController { public function __construct( string $appName, IRequest $request, @@ -53,6 +56,7 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController { private ?string $userId, private IRootFolder $rootFolder, private IAppData $appData, + private IMimeTypeDetector $mimeTypeDetector, ) { parent::__construct($appName, $request); } @@ -67,31 +71,70 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController { #[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_values(array_map(function ($descriptor) { + $tt['inputShape'] = array_map(function ($descriptor) { return $descriptor->jsonSerialize(); - }, $tt['inputShape'])); - $tt['outputShape'] = array_values(array_map(function ($descriptor) { + }, $tt['inputShape']); + if (empty($tt['inputShape'])) { + $tt['inputShape'] = new stdClass; + } + + $tt['outputShape'] = array_map(function ($descriptor) { return $descriptor->jsonSerialize(); - }, $tt['outputShape'])); - $tt['optionalInputShape'] = array_values(array_map(function ($descriptor) { + }, $tt['outputShape']); + if (empty($tt['outputShape'])) { + $tt['outputShape'] = new stdClass; + } + + $tt['optionalInputShape'] = array_map(function ($descriptor) { return $descriptor->jsonSerialize(); - }, $tt['optionalInputShape'])); - $tt['optionalOutputShape'] = array_values(array_map(function ($descriptor) { + }, $tt['optionalInputShape']); + if (empty($tt['optionalInputShape'])) { + $tt['optionalInputShape'] = new stdClass; + } + + $tt['optionalOutputShape'] = array_map(function ($descriptor) { return $descriptor->jsonSerialize(); - }, $tt['optionalOutputShape'])); - $tt['inputShapeEnumValues'] = array_values(array_map(function (array $enumValues) { - return array_values(array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues)); - }, $tt['inputShapeEnumValues'])); - $tt['optionalInputShapeEnumValues'] = array_values(array_map(function (array $enumValues) { - return array_values(array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues)); - }, $tt['optionalInputShapeEnumValues'])); - $tt['outputShapeEnumValues'] = array_values(array_map(function (array $enumValues) { - return array_values(array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues)); - }, $tt['outputShapeEnumValues'])); - $tt['optionalOutputShapeEnumValues'] = array_values(array_map(function (array $enumValues) { - return array_values(array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues)); - }, $tt['optionalOutputShapeEnumValues'])); + }, $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([ @@ -260,20 +303,22 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController { * * @param int $taskId The id of the task * @param int $fileId The file id of the file to retrieve - * @return DataDownloadResponse<Http::STATUS_OK, string, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}> + * @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] - #[Http\Attribute\NoCSRFRequired] + #[NoCSRFRequired] #[ApiRoute(verb: 'GET', url: '/tasks/{taskId}/file/{fileId}', root: '/taskprocessing')] - public function getFileContents(int $taskId, int $fileId): Http\DataDownloadResponse|DataResponse { + 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); } @@ -284,19 +329,21 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController { * * @param int $taskId The id of the task * @param int $fileId The file id of the file to retrieve - * @return DataDownloadResponse<Http::STATUS_OK, string, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}> + * @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): Http\DataDownloadResponse|DataResponse { + 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); } @@ -339,12 +386,11 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController { /** * @throws NotPermittedException * @throws NotFoundException - * @throws GenericFileException * @throws LockedException * - * @return DataDownloadResponse<Http::STATUS_OK, string, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}> + * @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): Http\DataDownloadResponse|DataResponse { + 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); @@ -361,7 +407,25 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController { } elseif (!$node instanceof File) { throw new NotFoundException('Node is not a file'); } - return new Http\DataDownloadResponse($node->getContent(), $node->getName(), $node->getMimeType()); + + $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; } /** diff --git a/core/Controller/TeamsApiController.php b/core/Controller/TeamsApiController.php index 36685555d4d..2eb33a0c254 100644 --- a/core/Controller/TeamsApiController.php +++ b/core/Controller/TeamsApiController.php @@ -13,6 +13,7 @@ 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; @@ -22,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, diff --git a/core/Controller/TextProcessingApiController.php b/core/Controller/TextProcessingApiController.php index cdf39563167..d3e6967f169 100644 --- a/core/Controller/TextProcessingApiController.php +++ b/core/Controller/TextProcessingApiController.php @@ -19,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; @@ -36,7 +37,7 @@ 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, diff --git a/core/Controller/TextToImageApiController.php b/core/Controller/TextToImageApiController.php index 3ffc868e80f..d2c3e1ec288 100644 --- a/core/Controller/TextToImageApiController.php +++ b/core/Controller/TextToImageApiController.php @@ -21,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; @@ -34,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, diff --git a/core/Controller/TranslationApiController.php b/core/Controller/TranslationApiController.php index 294251baa47..73dd0657230 100644 --- a/core/Controller/TranslationApiController.php +++ b/core/Controller/TranslationApiController.php @@ -17,13 +17,14 @@ 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, 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 ef0f420fc82..4791139bb12 100644 --- a/core/Controller/TwoFactorChallengeController.php +++ b/core/Controller/TwoFactorChallengeController.php @@ -25,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)] @@ -89,6 +90,7 @@ 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'); } @@ -141,6 +143,7 @@ class TwoFactorChallengeController extends Controller { if ($provider instanceof IProvidesCustomCSP) { $response->setContentSecurityPolicy($provider->getCSP()); } + Util::addScript('core', 'twofactor-request-token'); return $response; } @@ -204,6 +207,7 @@ class TwoFactorChallengeController extends Controller { 'redirect_url' => $redirect_url, ]; + Util::addScript('core', 'twofactor-request-token'); return new StandaloneTemplateResponse($this->appName, 'twofactorsetupselection', $data, 'guest'); } @@ -235,6 +239,7 @@ class TwoFactorChallengeController extends Controller { 'template' => $tmpl->fetchPage(), ]; $response = new StandaloneTemplateResponse($this->appName, 'twofactorsetupchallenge', $data, 'guest'); + Util::addScript('core', 'twofactor-request-token'); return $response; } diff --git a/core/Controller/WalledGardenController.php b/core/Controller/WalledGardenController.php index b55e90675a1..d0bc0665534 100644 --- a/core/Controller/WalledGardenController.php +++ b/core/Controller/WalledGardenController.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/core/Controller/WhatsNewController.php b/core/Controller/WhatsNewController.php index 86192d8f466..af8c3d4853b 100644 --- a/core/Controller/WhatsNewController.php +++ b/core/Controller/WhatsNewController.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -19,6 +20,7 @@ use OCP\IRequest; use OCP\IUserManager; use OCP\IUserSession; use OCP\L10N\IFactory; +use OCP\PreConditionNotMetException; use OCP\ServerVersion; class WhatsNewController extends OCSController { @@ -88,7 +90,7 @@ class WhatsNewController extends OCSController { * @param string $version Version to dismiss the changes for * * @return DataResponse<Http::STATUS_OK, list<empty>, array{}> - * @throws \OCP\PreConditionNotMetException + * @throws PreConditionNotMetException * @throws DoesNotExistException * * 200: Changes dismissed diff --git a/core/Controller/WipeController.php b/core/Controller/WipeController.php index d364e6399d9..1b57be71aa0 100644 --- a/core/Controller/WipeController.php +++ b/core/Controller/WipeController.php @@ -14,11 +14,13 @@ 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, |