diff --git a/apps/files_sharing/lib/Capabilities.php b/apps/files_sharing/lib/Capabilities.php index 1f4219ed1a5..1f491216abe 100644 --- a/apps/files_sharing/lib/Capabilities.php +++ b/apps/files_sharing/lib/Capabilities.php @@ -55,6 +55,7 @@ class Capabilities implements ICapability { * send_mail?: bool, * upload?: bool, * upload_files_drop?: bool, + * custom_tokens?: bool, * }, * user: array{ * send_mail: bool, @@ -136,6 +137,7 @@ class Capabilities implements ICapability { $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 ff20a0acf3c..f0b89ab5d47 100644 --- a/apps/files_sharing/lib/Controller/ShareAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareAPIController.php @@ -21,6 +21,7 @@ use OCA\Files_Sharing\SharedStorage; 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\Lock\LockedException; 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 @@ class ShareAPIController extends OCSController { * 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 @@ class ShareAPIController extends OCSController { ?string $hideDownload = null, ?string $attributes = null, ?string $sendMail = null, + ?string $token = null, ): DataResponse { try { $share = $this->getShareById($id); @@ -1211,7 +1215,8 @@ class ShareAPIController extends OCSController { $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 @@ class ShareAPIController extends OCSController { } 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 @@ class ShareAPIController extends OCSController { 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 @@ class ShareAPIController extends OCSController { 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 a9ad361ff35..145847b9550 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 bfc6357240d..28631dc7288 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 94db0454428..09fdca13598 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 00000000000..c497531dfdb --- /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 a50a3abf6bf..f50a533eeeb 100644 --- a/apps/files_sharing/src/views/SharingDetailsTab.vue +++ b/apps/files_sharing/src/views/SharingDetailsTab.vue @@ -105,9 +105,23 @@ role="region">
+ + +