diff --git a/apps/files_sharing/lib/Capabilities.php b/apps/files_sharing/lib/Capabilities.php index 1f4219ed1a57d..1f491216abe6d 100644 --- a/apps/files_sharing/lib/Capabilities.php +++ b/apps/files_sharing/lib/Capabilities.php @@ -55,6 +55,7 @@ public function __construct( * send_mail?: bool, * upload?: bool, * upload_files_drop?: bool, + * custom_tokens?: bool, * }, * user: array{ * send_mail: bool, @@ -136,6 +137,7 @@ public function getCapabilities() { $public['send_mail'] = $this->config->getAppValue('core', 'shareapi_allow_public_notification', 'no') === 'yes'; $public['upload'] = $this->shareManager->shareApiLinkAllowPublicUpload(); $public['upload_files_drop'] = $public['upload']; + $public['custom_tokens'] = $this->shareManager->allowCustomTokens(); } $res['public'] = $public; diff --git a/apps/files_sharing/lib/Controller/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php index ff20a0acf3cc3..f0b89ab5d4778 100644 --- a/apps/files_sharing/lib/Controller/ShareAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareAPIController.php @@ -21,6 +21,7 @@ use OCA\GlobalSiteSelector\Service\SlaveService; use OCP\App\IAppManager; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\ApiRoute; use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\Attribute\UserRateLimit; use OCP\AppFramework\Http\DataResponse; @@ -52,6 +53,7 @@ use OCP\Mail\IMailer; use OCP\Server; use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\Exceptions\ShareTokenException; use OCP\Share\IManager; use OCP\Share\IProviderFactory; use OCP\Share\IShare; @@ -1164,6 +1166,7 @@ private function hasPermission(int $permissionsSet, int $permissionsToCheck): bo * Considering the share already exists, no mail will be send after the share is updated. * You will have to use the sendMail action to send the mail. * @param string|null $shareWith New recipient for email shares + * @param string|null $token New token * @return DataResponse * @throws OCSBadRequestException Share could not be updated because the requested changes are invalid * @throws OCSForbiddenException Missing permissions to update the share @@ -1184,6 +1187,7 @@ public function updateShare( ?string $hideDownload = null, ?string $attributes = null, ?string $sendMail = null, + ?string $token = null, ): DataResponse { try { $share = $this->getShareById($id); @@ -1211,7 +1215,8 @@ public function updateShare( $label === null && $hideDownload === null && $attributes === null && - $sendMail === null + $sendMail === null && + $token === null ) { throw new OCSBadRequestException($this->l->t('Wrong or no update parameter given')); } @@ -1324,6 +1329,16 @@ public function updateShare( } elseif ($sendPasswordByTalk !== null) { $share->setSendPasswordByTalk(false); } + + if ($token !== null) { + if (!$this->shareManager->allowCustomTokens()) { + throw new OCSForbiddenException($this->l->t('Custom share link tokens have been disabled by the administrator')); + } + if (!$this->validateToken($token)) { + throw new OCSBadRequestException($this->l->t('Tokens must contain at least 1 character and may only contain letters, numbers, or a hyphen')); + } + $share->setToken($token); + } } // NOT A LINK SHARE @@ -1357,6 +1372,16 @@ public function updateShare( return new DataResponse($this->formatShare($share)); } + private function validateToken(string $token): bool { + if (mb_strlen($token) === 0) { + return false; + } + if (!preg_match('/^[a-z0-9-]+$/i', $token)) { + return false; + } + return true; + } + /** * Get all shares that are still pending * @@ -2152,4 +2177,26 @@ public function sendShareEmail(string $id, $password = ''): DataResponse { throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist')); } } + + /** + * Get a unique share token + * + * @throws OCSException Failed to generate a unique token + * + * @return DataResponse + * + * 200: Token generated successfully + */ + #[ApiRoute(verb: 'GET', url: '/api/v1/token')] + #[NoAdminRequired] + public function generateToken(): DataResponse { + try { + $token = $this->shareManager->generateToken(); + return new DataResponse([ + 'token' => $token, + ]); + } catch (ShareTokenException $e) { + throw new OCSException($this->l->t('Failed to generate a unique token')); + } + } } diff --git a/apps/files_sharing/openapi.json b/apps/files_sharing/openapi.json index a9ad361ff350e..145847b955044 100644 --- a/apps/files_sharing/openapi.json +++ b/apps/files_sharing/openapi.json @@ -129,6 +129,9 @@ }, "upload_files_drop": { "type": "boolean" + }, + "custom_tokens": { + "type": "boolean" } } }, @@ -2313,6 +2316,11 @@ "type": "string", "nullable": true, "description": "if the share should be send by mail. Considering the share already exists, no mail will be send after the share is updated. You will have to use the sendMail action to send the mail." + }, + "token": { + "type": "string", + "nullable": true, + "description": "New token" } } } @@ -3833,6 +3841,75 @@ } } } + }, + "/ocs/v2.php/apps/files_sharing/api/v1/token": { + "get": { + "operationId": "shareapi-generate-token", + "summary": "Get a unique share token", + "tags": [ + "shareapi" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Token generated successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } } }, "tags": [] diff --git a/apps/files_sharing/src/models/Share.ts b/apps/files_sharing/src/models/Share.ts index bfc6357240d2f..28631dc7288cb 100644 --- a/apps/files_sharing/src/models/Share.ts +++ b/apps/files_sharing/src/models/Share.ts @@ -192,6 +192,13 @@ export default class Share { return this._share.token } + /** + * Set the public share token + */ + set token(token: string) { + this._share.token = token + } + /** * Get the share note if any */ diff --git a/apps/files_sharing/src/services/ConfigService.ts b/apps/files_sharing/src/services/ConfigService.ts index 94db045442841..09fdca135989f 100644 --- a/apps/files_sharing/src/services/ConfigService.ts +++ b/apps/files_sharing/src/services/ConfigService.ts @@ -34,7 +34,8 @@ type FileSharingCapabilities = { }, send_mail: boolean, upload: boolean, - upload_files_drop: boolean + upload_files_drop: boolean, + custom_tokens: boolean, }, resharing: boolean, user: { @@ -298,4 +299,11 @@ export default class Config { return this._capabilities?.password_policy || {} } + /** + * Returns true if custom tokens are allowed + */ + get allowCustomTokens(): boolean { + return this._capabilities?.files_sharing?.public?.custom_tokens + } + } diff --git a/apps/files_sharing/src/services/TokenService.ts b/apps/files_sharing/src/services/TokenService.ts new file mode 100644 index 0000000000000..c497531dfdb82 --- /dev/null +++ b/apps/files_sharing/src/services/TokenService.ts @@ -0,0 +1,20 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import axios from '@nextcloud/axios' +import { generateOcsUrl } from '@nextcloud/router' + +interface TokenData { + ocs: { + data: { + token: string, + } + } +} + +export const generateToken = async (): Promise => { + const { data } = await axios.get(generateOcsUrl('/apps/files_sharing/api/v1/token')) + return data.ocs.data.token +} diff --git a/apps/files_sharing/src/views/SharingDetailsTab.vue b/apps/files_sharing/src/views/SharingDetailsTab.vue index a50a3abf6bffc..f50a533eeeb44 100644 --- a/apps/files_sharing/src/views/SharingDetailsTab.vue +++ b/apps/files_sharing/src/views/SharingDetailsTab.vue @@ -105,9 +105,23 @@ role="region">
+ + +