From f92875cf6acc698c81c9b1b6959536d3f80d28de Mon Sep 17 00:00:00 2001 From: Christopher Ng Date: Mon, 16 Dec 2024 15:53:12 -0800 Subject: [PATCH 01/12] feat(sharing): Allow updating share token Signed-off-by: Christopher Ng --- .../lib/Controller/ShareAPIController.php | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/apps/files_sharing/lib/Controller/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php index ff20a0acf3cc3..57022f7ea1c74 100644 --- a/apps/files_sharing/lib/Controller/ShareAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareAPIController.php @@ -1164,6 +1164,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 +1185,7 @@ public function updateShare( ?string $hideDownload = null, ?string $attributes = null, ?string $sendMail = null, + ?string $token = null, ): DataResponse { try { $share = $this->getShareById($id); @@ -1211,7 +1213,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 +1327,13 @@ public function updateShare( } elseif ($sendPasswordByTalk !== null) { $share->setSendPasswordByTalk(false); } + + if ($token !== null) { + 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 +1367,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 * From beffb26266c509c89c1d7c96af560392d15f03f7 Mon Sep 17 00:00:00 2001 From: Christopher Ng Date: Mon, 16 Dec 2024 15:53:12 -0800 Subject: [PATCH 02/12] feat(sharing): Fetch a unique generated token Signed-off-by: Christopher Ng --- .../lib/Controller/ShareAPIController.php | 24 ++++++ lib/private/Share20/Manager.php | 76 ++++++++++--------- .../Share/Exceptions/ShareTokenException.php | 16 ++++ lib/public/Share/IManager.php | 9 +++ 4 files changed, 90 insertions(+), 35 deletions(-) create mode 100644 lib/public/Share/Exceptions/ShareTokenException.php diff --git a/apps/files_sharing/lib/Controller/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php index 57022f7ea1c74..b00c97f0dd3c6 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; @@ -2172,4 +2174,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/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index 219f3d86380a5..4138819f6b161 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -43,6 +43,7 @@ use OCP\Share\Exceptions\AlreadySharedException; use OCP\Share\Exceptions\GenericShareException; use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\Exceptions\ShareTokenException; use OCP\Share\IManager; use OCP\Share\IProviderFactory; use OCP\Share\IShare; @@ -659,41 +660,7 @@ public function createShare(IShare $share) { $this->linkCreateChecks($share); $this->setLinkParent($share); - // Initial token length - $tokenLength = \OC\Share\Helper::getTokenLength(); - - do { - $tokenExists = false; - - for ($i = 0; $i <= 2; $i++) { - // Generate a new token - $token = $this->secureRandom->generate( - $tokenLength, - \OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE - ); - - try { - // Try to fetch a share with the generated token - $this->getShareByToken($token); - $tokenExists = true; // Token exists, we need to try again - } catch (\OCP\Share\Exceptions\ShareNotFound $e) { - // Token is unique, exit the loop - $tokenExists = false; - break; - } - } - - // If we've reached the maximum attempts and the token still exists, increase the token length - if ($tokenExists) { - $tokenLength++; - - // Check if the token length exceeds the maximum allowed length - if ($tokenLength > \OC\Share\Constants::MAX_TOKEN_LENGTH) { - throw new \Exception('Unable to generate a unique share token. Maximum token length exceeded.'); - } - } - } while ($tokenExists); - + $token = $this->generateToken(); // Set the unique token $share->setToken($token); @@ -2025,4 +1992,43 @@ public function getAllShares(): iterable { yield from $provider->getAllShares(); } } + + public function generateToken(): string { + // Initial token length + $tokenLength = \OC\Share\Helper::getTokenLength(); + + do { + $tokenExists = false; + + for ($i = 0; $i <= 2; $i++) { + // Generate a new token + $token = $this->secureRandom->generate( + $tokenLength, + ISecureRandom::CHAR_HUMAN_READABLE, + ); + + try { + // Try to fetch a share with the generated token + $this->getShareByToken($token); + $tokenExists = true; // Token exists, we need to try again + } catch (ShareNotFound $e) { + // Token is unique, exit the loop + $tokenExists = false; + break; + } + } + + // If we've reached the maximum attempts and the token still exists, increase the token length + if ($tokenExists) { + $tokenLength++; + + // Check if the token length exceeds the maximum allowed length + if ($tokenLength > \OC\Share\Constants::MAX_TOKEN_LENGTH) { + throw new ShareTokenException('Unable to generate a unique share token. Maximum token length exceeded.'); + } + } + } while ($tokenExists); + + return $token; + } } diff --git a/lib/public/Share/Exceptions/ShareTokenException.php b/lib/public/Share/Exceptions/ShareTokenException.php new file mode 100644 index 0000000000000..027d00640e9a0 --- /dev/null +++ b/lib/public/Share/Exceptions/ShareTokenException.php @@ -0,0 +1,16 @@ + Date: Mon, 16 Dec 2024 15:53:12 -0800 Subject: [PATCH 03/12] chore(openapi): Update spec Signed-off-by: Christopher Ng --- apps/files_sharing/openapi.json | 74 +++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/apps/files_sharing/openapi.json b/apps/files_sharing/openapi.json index a9ad361ff350e..36c969a344029 100644 --- a/apps/files_sharing/openapi.json +++ b/apps/files_sharing/openapi.json @@ -2313,6 +2313,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 +3838,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": [] From cdc65b69854a7a87130b290566c992a4b1881498 Mon Sep 17 00:00:00 2001 From: Christopher Ng Date: Mon, 16 Dec 2024 15:53:12 -0800 Subject: [PATCH 04/12] feat(sharing): Make it possible to customize share link tokens Signed-off-by: Christopher Ng --- apps/files_sharing/src/models/Share.ts | 7 +++ .../src/services/TokenService.ts | 20 ++++++++ .../src/views/SharingDetailsTab.vue | 47 ++++++++++++++++++- 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 apps/files_sharing/src/services/TokenService.ts 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/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..4d46ec3279624 100644 --- a/apps/files_sharing/src/views/SharingDetailsTab.vue +++ b/apps/files_sharing/src/views/SharingDetailsTab.vue @@ -105,9 +105,23 @@ role="region">
+ + +