diff --git a/docs/API_v3.md b/docs/API_v3.md index 40c61d54c..ce4ba2e00 100644 --- a/docs/API_v3.md +++ b/docs/API_v3.md @@ -631,7 +631,12 @@ Update a single or all properties of an option-object | Parameter | Type | Description | |------------------|----------|-------------| | _keyValuePairs_ | Array | Array of key-value pairs to update | -- Restrictions: Currently only the _permissions_ can be updated. +- Restrictions: + - Allowed keys are _permissions_ and _token_. + - _token_ updates are only available when the admin setting _allowCustomPublicShareTokens_ is enabled. + - _token_ can only be updated on link shares. + - _token_ must be unique among link shares and only contain alphanumeric characters. + - _token_ length must be between 8 and 256 characters. - Response: **Status-Code OK**, as well as the id of the share object. ``` diff --git a/lib/Constants.php b/lib/Constants.php index 0ff0341bd..d734d6c76 100644 --- a/lib/Constants.php +++ b/lib/Constants.php @@ -15,17 +15,23 @@ class Constants { */ public const CONFIG_KEY_ALLOWPERMITALL = 'allowPermitAll'; public const CONFIG_KEY_ALLOWPUBLICLINK = 'allowPublicLink'; + public const CONFIG_KEY_ALLOWCUSTOMPUBLICTOKEN = 'allowCustomPublicShareTokens'; public const CONFIG_KEY_ALLOWSHOWTOALL = 'allowShowToAll'; public const CONFIG_KEY_CREATIONALLOWEDGROUPS = 'creationAllowedGroups'; public const CONFIG_KEY_RESTRICTCREATION = 'restrictCreation'; public const CONFIG_KEYS = [ self::CONFIG_KEY_ALLOWPERMITALL, self::CONFIG_KEY_ALLOWPUBLICLINK, + self::CONFIG_KEY_ALLOWCUSTOMPUBLICTOKEN, self::CONFIG_KEY_ALLOWSHOWTOALL, self::CONFIG_KEY_CREATIONALLOWEDGROUPS, self::CONFIG_KEY_RESTRICTCREATION ]; + public const PUBLIC_SHARE_TOKEN_MIN_LENGTH = 8; + public const PUBLIC_SHARE_TOKEN_MAX_LENGTH = 256; + public const PUBLIC_SHARE_HASH_REQUIREMENT = '[a-zA-Z0-9]{' . self::PUBLIC_SHARE_TOKEN_MIN_LENGTH . ',' . self::PUBLIC_SHARE_TOKEN_MAX_LENGTH . '}'; + /** * Maximum String lengths, the database is set to store. */ diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index 43db2366a..c353d6638 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -145,7 +145,7 @@ public function internalLinkView(string $hash): Response { #[NoAdminRequired()] #[NoCSRFRequired()] #[PublicPage()] - #[FrontpageRoute(verb: 'GET', url: '/s/{hash}', requirements: ['hash' => '[a-zA-Z0-9]{24,}'])] + #[FrontpageRoute(verb: 'GET', url: '/s/{hash}', requirements: ['hash' => Constants::PUBLIC_SHARE_HASH_REQUIREMENT])] public function publicLinkView(string $hash): Response { try { $share = $this->shareMapper->findPublicShareByHash($hash); diff --git a/lib/Controller/ShareApiController.php b/lib/Controller/ShareApiController.php index badf724f9..a341d4b9e 100644 --- a/lib/Controller/ShareApiController.php +++ b/lib/Controller/ShareApiController.php @@ -207,7 +207,7 @@ public function newShare(int $formId, int $shareType, string $shareWith = '', ar } /** - * Update permissions of a share + * Update properties of a share * * @param int $formId of the form * @param int $shareId of the share to update @@ -215,9 +215,13 @@ public function newShare(int $formId, int $shareType, string $shareWith = '', ar * @return DataResponse * @throws OCSBadRequestException Share doesn't belong to given Form * @throws OCSBadRequestException Invalid permission given + * @throws OCSBadRequestException Invalid share token + * @throws OCSBadRequestException Share hash exists, please retry + * @throws OCSForbiddenException Custom public share tokens are not allowed * @throws OCSForbiddenException This form is not owned by the current user * @throws OCSForbiddenException Empty keyValuePairs, will not update - * @throws OCSForbiddenException Not allowed to update other properties than permissions + * @throws OCSForbiddenException Not allowed to update token on non-link share + * @throws OCSForbiddenException Not allowed to update unknown properties * @throws OCSNotFoundException Could not find share * * 200: the id of the updated share @@ -226,7 +230,7 @@ public function newShare(int $formId, int $shareType, string $shareWith = '', ar #[NoAdminRequired()] #[ApiRoute(verb: 'PATCH', url: '/api/v3/forms/{formId}/shares/{shareId}')] public function updateShare(int $formId, int $shareId, array $keyValuePairs): DataResponse { - $this->logger->debug('Updating share: {shareId} of form {formId}, permissions: {permissions}', [ + $this->logger->debug('Updating share: {shareId} of form {formId}, values: {keyValuePairs}', [ 'formId' => $formId, 'shareId' => $shareId, 'keyValuePairs' => $keyValuePairs @@ -256,22 +260,64 @@ public function updateShare(int $formId, int $shareId, array $keyValuePairs): Da throw new OCSForbiddenException('Empty keyValuePairs, will not update'); } - //Don't allow to change other properties than permissions - if (count($keyValuePairs) > 1 || !array_key_exists('permissions', $keyValuePairs)) { - $this->logger->debug('Not allowed to update other properties than permissions'); - throw new OCSForbiddenException('Not allowed to update other properties than permissions'); + $allowedKeys = ['permissions', 'token']; + foreach (array_keys($keyValuePairs) as $key) { + if (!in_array($key, $allowedKeys, true)) { + $this->logger->debug('Not allowed to update unknown properties'); + throw new OCSForbiddenException('Not allowed to update unknown properties'); + } } - if (!$this->validatePermissions($keyValuePairs['permissions'], $formShare->getShareType())) { + if (array_key_exists('permissions', $keyValuePairs) && !$this->validatePermissions($keyValuePairs['permissions'], $formShare->getShareType())) { throw new OCSBadRequestException('Invalid permission given'); } + if (array_key_exists('token', $keyValuePairs)) { + if (!$this->configService->getAllowCustomPublicToken()) { + $this->logger->debug('Custom public share tokens are not allowed.'); + throw new OCSForbiddenException('Custom public share tokens are not allowed.'); + } + + if ($formShare->getShareType() !== IShare::TYPE_LINK) { + $this->logger->debug('Not allowed to update token on non-link share'); + throw new OCSForbiddenException('Not allowed to update token on non-link share'); + } + + if (!is_string($keyValuePairs['token'])) { + throw new OCSBadRequestException('Invalid share token'); + } + + $token = $keyValuePairs['token']; + if (!array_key_exists('permissions', $keyValuePairs) && $token === $formShare->getShareWith()) { + return new DataResponse($formShare->getId()); + } + + if ($token !== $formShare->getShareWith()) { + $this->validatePublicShareToken($token); + + try { + $existingShare = $this->shareMapper->findPublicShareByHash($token); + if ($existingShare->getId() !== $formShare->getId()) { + $this->logger->debug('Share hash already exists.'); + throw new OCSBadRequestException('Share hash exists, please retry.'); + } + } catch (DoesNotExistException $e) { + // Just continue, this is what we expect to happen (share hash not existing yet). + } + + $formShare->setShareWith($token); + } + } + $this->formsService->obtainFormLock($form); - $formShare->setPermissions($keyValuePairs['permissions']); + if (array_key_exists('permissions', $keyValuePairs)) { + $formShare->setPermissions($keyValuePairs['permissions']); + } $formShare = $this->shareMapper->update($formShare); - if (in_array($formShare->getShareType(), [IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_USERGROUP, IShare::TYPE_CIRCLE], true)) { + if (array_key_exists('permissions', $keyValuePairs) + && in_array($formShare->getShareType(), [IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_USERGROUP, IShare::TYPE_CIRCLE], true)) { if (in_array(Constants::PERMISSION_RESULTS, $keyValuePairs['permissions'], true)) { $userFolder = $this->rootFolder->getUserFolder($form->getOwnerId()); $uploadedFilesFolderPath = $this->formsService->getFormUploadedFilesFolderPath($form); @@ -421,4 +467,22 @@ private function validatePermissions(array $permissions, int $shareType): bool { } return true; } + + /** + * @throws OCSBadRequestException If token does not satisfy basic safety checks + */ + private function validatePublicShareToken(string $token): void { + if ($token !== trim($token)) { + throw new OCSBadRequestException('Invalid share token'); + } + + $tokenLength = strlen($token); + if ($tokenLength < Constants::PUBLIC_SHARE_TOKEN_MIN_LENGTH || $tokenLength > Constants::PUBLIC_SHARE_TOKEN_MAX_LENGTH) { + throw new OCSBadRequestException('Invalid share token'); + } + + if (preg_match('/^[a-zA-Z0-9]+$/', $token) !== 1) { + throw new OCSBadRequestException('Invalid share token'); + } + } } diff --git a/lib/Service/ConfigService.php b/lib/Service/ConfigService.php index 593315c9e..1b3891b2d 100644 --- a/lib/Service/ConfigService.php +++ b/lib/Service/ConfigService.php @@ -37,7 +37,10 @@ public function getAllowPermitAll(): bool { public function getAllowPublicLink(): bool { return json_decode($this->config->getAppValue($this->appName, Constants::CONFIG_KEY_ALLOWPUBLICLINK, 'true')); } - public function getAllowShowToAll() : bool { + public function getAllowCustomPublicToken(): bool { + return json_decode($this->config->getAppValue($this->appName, Constants::CONFIG_KEY_ALLOWCUSTOMPUBLICTOKEN, 'false')); + } + public function getAllowShowToAll(): bool { return json_decode($this->config->getAppValue($this->appName, Constants::CONFIG_KEY_ALLOWSHOWTOALL, 'true')); } private function getUnformattedCreationAllowedGroups(): array { @@ -57,6 +60,7 @@ public function getAppConfig(): array { return [ Constants::CONFIG_KEY_ALLOWPERMITALL => $this->getAllowPermitAll(), Constants::CONFIG_KEY_ALLOWPUBLICLINK => $this->getAllowPublicLink(), + Constants::CONFIG_KEY_ALLOWCUSTOMPUBLICTOKEN => $this->getAllowCustomPublicToken(), Constants::CONFIG_KEY_ALLOWSHOWTOALL => $this->getAllowShowToAll(), Constants::CONFIG_KEY_CREATIONALLOWEDGROUPS => $this->getCreationAllowedGroups(), Constants::CONFIG_KEY_RESTRICTCREATION => $this->getRestrictCreation(), diff --git a/openapi.json b/openapi.json index 9d80a1151..b5870638a 100644 --- a/openapi.json +++ b/openapi.json @@ -5157,7 +5157,7 @@ "/ocs/v2.php/apps/forms/api/v3/forms/{formId}/shares/{shareId}": { "patch": { "operationId": "share_api-update-share", - "summary": "Update permissions of a share", + "summary": "Update properties of a share", "description": "This endpoint allows CORS requests", "tags": [ "share_api" @@ -5254,7 +5254,7 @@ } }, "400": { - "description": "Invalid permission given", + "description": "Share hash exists, please retry", "content": { "application/json": { "schema": { @@ -5282,7 +5282,7 @@ } }, "403": { - "description": "Not allowed to update other properties than permissions", + "description": "Not allowed to update unknown properties", "content": { "application/json": { "schema": { diff --git a/src/FormsSettings.vue b/src/FormsSettings.vue index 60c607c8e..bd26a8eca 100644 --- a/src/FormsSettings.vue +++ b/src/FormsSettings.vue @@ -32,6 +32,13 @@ @update:modelValue="onAllowPublicLinkChange"> {{ t('forms', 'Allow sharing by link') }} + + {{ t('forms', 'Allow custom public share tokens') }} + - + {{ t('forms', 'Show QR code') }} + + + {{ t('forms', 'Save token') }} + @@ -212,7 +238,9 @@ import NcActionLink from '@nextcloud/vue/components/NcActionLink' import NcActions from '@nextcloud/vue/components/NcActions' import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' +import NcTextField from '@nextcloud/vue/components/NcTextField' import IconAccountMultiple from 'vue-material-design-icons/AccountMultipleOutline.vue' +import IconCheck from 'vue-material-design-icons/Check.vue' import IconCodeBrackets from 'vue-material-design-icons/CodeBrackets.vue' import IconLinkVariant from 'vue-material-design-icons/Link.vue' import IconLinkBoxVariantOutline from 'vue-material-design-icons/LinkBoxOutline.vue' @@ -234,6 +262,7 @@ export default { components: { FormsIcon, IconAccountMultiple, + IconCheck, IconCodeBrackets, IconCopyAll, IconDelete, @@ -246,6 +275,7 @@ export default { NcActionLink, NcCheckboxRadioSwitch, NcNoteCard, + NcTextField, QRDialog, SharingSearchDiv, SharingShareDiv, @@ -276,10 +306,27 @@ export default { return { isLoading: false, appConfig: loadState(appName, 'appConfig'), + shareTokens: {}, + savingShareTokens: {}, qrDialogText: '', } }, + watch: { + publicLinkShares: { + immediate: true, + handler(shares) { + const nextShareTokens = {} + for (const share of shares) { + nextShareTokens[share.id] = + this.shareTokens[share.id] ?? share.shareWith + } + + this.shareTokens = nextShareTokens + }, + }, + }, + computed: { isCurrentUserOwner() { return getCurrentUser().uid === this.form.ownerId @@ -480,6 +527,75 @@ export default { this.$emit('update:formProp', 'access', newAccess) }, + getShareTokenInput(share) { + return this.shareTokens[share.id] ?? share.shareWith + }, + + setShareTokenInput(share, value) { + this.shareTokens = { + ...this.shareTokens, + [share.id]: value, + } + }, + + isShareTokenSaving(share) { + return !!this.savingShareTokens[share.id] + }, + + isShareTokenDirty(share) { + return this.getShareTokenInput(share).trim() !== share.shareWith + }, + + async updateShareToken(share) { + const token = this.getShareTokenInput(share).trim() + if (token === share.shareWith) { + return + } + + this.isLoading = true + this.savingShareTokens = { + ...this.savingShareTokens, + [share.id]: true, + } + + try { + const response = await axios.patch( + generateOcsUrl('apps/forms/api/v3/forms/{id}/shares/{shareId}', { + id: this.form.id, + shareId: share.id, + }), + { + keyValuePairs: { + token, + }, + }, + ) + + this.$emit('updateShare', { + ...share, + id: OcsResponse2Data(response), + shareWith: token, + }) + + this.setShareTokenInput(share, token) + } catch (error) { + logger.error('Error while updating share token', { + error, + share, + token, + }) + showError( + t('forms', 'There was an error while updating the link token'), + ) + } finally { + this.savingShareTokens = { + ...this.savingShareTokens, + [share.id]: false, + } + this.isLoading = false + } + }, + openQrDialog(share) { this.qrDialogText = this.getPublicShareLink(share) }, @@ -534,6 +650,13 @@ export default { padding: 0px 8px; flex-grow: 1; + &--tokenized { + display: flex; + flex-direction: column; + justify-content: center; + padding-block: 8px; + } + &--twoline { span { display: block; @@ -545,5 +668,9 @@ export default { } } } + + :deep(.share-div__desc--tokenized .input-field__main-wrapper) { + min-inline-size: 220px; + } } diff --git a/tests/Unit/Controller/ConfigControllerTest.php b/tests/Unit/Controller/ConfigControllerTest.php index 7e5020c6a..6a138d69c 100644 --- a/tests/Unit/Controller/ConfigControllerTest.php +++ b/tests/Unit/Controller/ConfigControllerTest.php @@ -69,16 +69,21 @@ public function testGetAppConfig() { public static function dataUpdateAppConfig() { return [ - 'booleanConfig' => [ + 'booleanConfigAllowPermitAll' => [ 'configKey' => 'allowPermitAll', 'configValue' => true, 'strConfig' => 'true' ], - 'booleanConfig' => [ + 'booleanConfigAllowShowToAll' => [ 'configKey' => 'allowShowToAll', 'configValue' => true, 'strConfig' => 'true' ], + 'booleanConfigAllowCustomPublicShareTokens' => [ + 'configKey' => 'allowCustomPublicShareTokens', + 'configValue' => true, + 'strConfig' => 'true' + ], 'arrayConfig' => [ 'configKey' => 'allowPermitAll', 'configValue' => [ diff --git a/tests/Unit/Controller/ShareApiControllerTest.php b/tests/Unit/Controller/ShareApiControllerTest.php index 3f3dad8e7..b7c371471 100644 --- a/tests/Unit/Controller/ShareApiControllerTest.php +++ b/tests/Unit/Controller/ShareApiControllerTest.php @@ -952,4 +952,351 @@ public function testUpdateShare_NotExistingForm() { $this->expectException(NoSuchFormException::class); $this->shareApiController->updateShare(7331, 1337, [Constants::PERMISSION_SUBMIT]); } + + public function testUpdateShareToken() { + $form = new Form(); + $form->setId('5'); + $form->setOwnerId('currentUser'); + + $this->configService->expects($this->once()) + ->method('getAllowCustomPublicToken') + ->willReturn(true); + + $this->formsService->expects($this->once()) + ->method('getFormIfAllowed') + ->with(5) + ->willReturn($form); + + $share = new Share(); + $share->setId(8); + $share->setFormId(5); + $share->setShareType(IShare::TYPE_LINK); + $share->setShareWith('abcdefgh'); + $share->setPermissions([Constants::PERMISSION_SUBMIT]); + + $this->shareMapper->expects($this->once()) + ->method('findById') + ->with(8) + ->willReturn($share); + + $this->shareMapper->expects($this->once()) + ->method('findPublicShareByHash') + ->with('tokenabcd') + ->willThrowException(new DoesNotExistException('Not found')); + + $this->shareMapper->expects($this->once()) + ->method('update') + ->willReturnCallback(function (Share $updatedShare) { + $this->assertSame('tokenabcd', $updatedShare->getShareWith()); + return $updatedShare; + }); + + $this->assertEquals(new DataResponse(8), $this->shareApiController->updateShare(5, 8, ['token' => 'tokenabcd'])); + } + + public function testUpdateShareToken_CustomTokensDisabled() { + $form = new Form(); + $form->setId('5'); + $form->setOwnerId('currentUser'); + + $this->formsService->expects($this->once()) + ->method('getFormIfAllowed') + ->with(5) + ->willReturn($form); + + $share = new Share(); + $share->setId(8); + $share->setFormId(5); + $share->setShareType(IShare::TYPE_LINK); + $share->setShareWith('abcdefgh'); + + $this->shareMapper->expects($this->once()) + ->method('findById') + ->with(8) + ->willReturn($share); + + $this->configService->expects($this->once()) + ->method('getAllowCustomPublicToken') + ->willReturn(false); + + $this->expectException(OCSForbiddenException::class); + $this->shareApiController->updateShare(5, 8, ['token' => 'tokenabcd']); + } + + public function testUpdateShareToken_ForbiddenForNonLinkShare() { + $form = new Form(); + $form->setId('5'); + $form->setOwnerId('currentUser'); + + $this->configService->expects($this->once()) + ->method('getAllowCustomPublicToken') + ->willReturn(true); + + $this->formsService->expects($this->once()) + ->method('getFormIfAllowed') + ->with(5) + ->willReturn($form); + + $share = new Share(); + $share->setId(8); + $share->setFormId(5); + $share->setShareType(IShare::TYPE_USER); + $share->setShareWith('user1'); + + $this->shareMapper->expects($this->once()) + ->method('findById') + ->with(8) + ->willReturn($share); + + $this->expectException(OCSForbiddenException::class); + $this->shareApiController->updateShare(5, 8, ['token' => 'tokenabcd']); + } + + public function testUpdateShareToken_DuplicateHash() { + $form = new Form(); + $form->setId('5'); + $form->setOwnerId('currentUser'); + + $this->configService->expects($this->once()) + ->method('getAllowCustomPublicToken') + ->willReturn(true); + + $this->formsService->expects($this->once()) + ->method('getFormIfAllowed') + ->with(5) + ->willReturn($form); + + $currentShare = new Share(); + $currentShare->setId(8); + $currentShare->setFormId(5); + $currentShare->setShareType(IShare::TYPE_LINK); + $currentShare->setShareWith('abcdefgh'); + + $existingShare = new Share(); + $existingShare->setId(9); + $existingShare->setFormId(5); + $existingShare->setShareType(IShare::TYPE_LINK); + $existingShare->setShareWith('tokenabcd'); + + $this->shareMapper->expects($this->once()) + ->method('findById') + ->with(8) + ->willReturn($currentShare); + + $this->shareMapper->expects($this->once()) + ->method('findPublicShareByHash') + ->with('tokenabcd') + ->willReturn($existingShare); + + $this->expectException(OCSBadRequestException::class); + $this->shareApiController->updateShare(5, 8, ['token' => 'tokenabcd']); + } + + public function testUpdateShareToken_InvalidToken() { + $form = new Form(); + $form->setId('5'); + $form->setOwnerId('currentUser'); + + $this->configService->expects($this->once()) + ->method('getAllowCustomPublicToken') + ->willReturn(true); + + $this->formsService->expects($this->once()) + ->method('getFormIfAllowed') + ->with(5) + ->willReturn($form); + + $share = new Share(); + $share->setId(8); + $share->setFormId(5); + $share->setShareType(IShare::TYPE_LINK); + $share->setShareWith('abcdefgh'); + + $this->shareMapper->expects($this->once()) + ->method('findById') + ->with(8) + ->willReturn($share); + + $this->shareMapper->expects($this->never()) + ->method('update'); + + $this->expectException(OCSBadRequestException::class); + $this->shareApiController->updateShare(5, 8, ['token' => 'invalid-token']); + } + + public function testUpdateShareToken_WhitespaceToken() { + $form = new Form(); + $form->setId('5'); + $form->setOwnerId('currentUser'); + + $this->configService->expects($this->once()) + ->method('getAllowCustomPublicToken') + ->willReturn(true); + + $this->formsService->expects($this->once()) + ->method('getFormIfAllowed') + ->with(5) + ->willReturn($form); + + $share = new Share(); + $share->setId(8); + $share->setFormId(5); + $share->setShareType(IShare::TYPE_LINK); + $share->setShareWith('abcdefgh'); + + $this->shareMapper->expects($this->once()) + ->method('findById') + ->with(8) + ->willReturn($share); + + $this->shareMapper->expects($this->never()) + ->method('findPublicShareByHash'); + + $this->expectException(OCSBadRequestException::class); + $this->shareApiController->updateShare(5, 8, ['token' => ' customtoken ']); + } + + public function testUpdateShareToken_TooShortToken() { + $form = new Form(); + $form->setId('5'); + $form->setOwnerId('currentUser'); + + $this->configService->expects($this->once()) + ->method('getAllowCustomPublicToken') + ->willReturn(true); + + $this->formsService->expects($this->once()) + ->method('getFormIfAllowed') + ->with(5) + ->willReturn($form); + + $share = new Share(); + $share->setId(8); + $share->setFormId(5); + $share->setShareType(IShare::TYPE_LINK); + $share->setShareWith('abcdefgh'); + + $this->shareMapper->expects($this->once()) + ->method('findById') + ->with(8) + ->willReturn($share); + + $this->shareMapper->expects($this->never()) + ->method('findPublicShareByHash'); + + $this->expectException(OCSBadRequestException::class); + $this->shareApiController->updateShare(5, 8, ['token' => 'abc']); + } + + public function testUpdateShareToken_SameTokenReturnsEarly() { + $form = new Form(); + $form->setId('5'); + $form->setOwnerId('currentUser'); + + $this->configService->expects($this->once()) + ->method('getAllowCustomPublicToken') + ->willReturn(true); + + $this->formsService->expects($this->once()) + ->method('getFormIfAllowed') + ->with(5) + ->willReturn($form); + + $share = new Share(); + $share->setId(8); + $share->setFormId(5); + $share->setShareType(IShare::TYPE_LINK); + $share->setShareWith('sameToken123'); + + $this->shareMapper->expects($this->once()) + ->method('findById') + ->with(8) + ->willReturn($share); + + $this->shareMapper->expects($this->never()) + ->method('findPublicShareByHash'); + $this->shareMapper->expects($this->never()) + ->method('update'); + $this->formsService->expects($this->never()) + ->method('obtainFormLock'); + + $this->assertEquals(new DataResponse(8), $this->shareApiController->updateShare(5, 8, ['token' => 'sameToken123'])); + } + + public function testUpdateShareToken_ForeignShare() { + $form = new Form(); + $form->setId('5'); + $form->setOwnerId('currentUser'); + + $this->configService->expects($this->never()) + ->method('getAllowCustomPublicToken') + ->willReturn(true); + + $this->formsService->expects($this->once()) + ->method('getFormIfAllowed') + ->with(5) + ->willReturn($form); + + $share = new Share(); + $share->setId(8); + $share->setFormId(6); + $share->setShareType(IShare::TYPE_LINK); + $share->setShareWith('abcdefgh'); + + $this->shareMapper->expects($this->once()) + ->method('findById') + ->with(8) + ->willReturn($share); + + $this->expectException(OCSBadRequestException::class); + $this->shareApiController->updateShare(5, 8, ['token' => 'customtoken123']); + } + + public function testUpdateShareToken_ShareNotFound() { + $form = new Form(); + $form->setId('5'); + $form->setOwnerId('currentUser'); + + $this->configService->expects($this->never()) + ->method('getAllowCustomPublicToken') + ->willReturn(true); + + $this->formsService->expects($this->once()) + ->method('getFormIfAllowed') + ->with(5) + ->willReturn($form); + + $this->shareMapper->expects($this->once()) + ->method('findById') + ->with(8) + ->willThrowException(new DoesNotExistException('missing')); + + $this->expectException(OCSNotFoundException::class); + $this->shareApiController->updateShare(5, 8, ['token' => 'customtoken123']); + } + + public function testUpdateShareToken_ArchivedForm() { + $form = new Form(); + $form->setId('5'); + $form->setOwnerId('currentUser'); + + $this->configService->expects($this->never()) + ->method('getAllowCustomPublicToken') + ->willReturn(true); + + $this->formsService->expects($this->once()) + ->method('getFormIfAllowed') + ->with(5) + ->willReturn($form); + $this->formsService->expects($this->once()) + ->method('isFormArchived') + ->with($form) + ->willReturn(true); + + $this->shareMapper->expects($this->never()) + ->method('findById'); + + $this->expectException(OCSForbiddenException::class); + $this->shareApiController->updateShare(5, 8, ['token' => 'customtoken123']); + } } diff --git a/tests/Unit/Service/ConfigServiceTest.php b/tests/Unit/Service/ConfigServiceTest.php index d0ff7a420..354dc7033 100644 --- a/tests/Unit/Service/ConfigServiceTest.php +++ b/tests/Unit/Service/ConfigServiceTest.php @@ -62,6 +62,7 @@ public static function dataGetAppConfig() { 'strConfig' => [ 'allowPermitAll' => 'false', 'allowPublicLink' => 'false', + 'allowCustomPublicShareTokens' => 'true', 'allowShowToAll' => 'false', 'creationAllowedGroups' => '["group1", "group2", "nonExisting"]', 'restrictCreation' => 'true', @@ -73,6 +74,7 @@ public static function dataGetAppConfig() { 'expected' => [ 'allowPermitAll' => false, 'allowPublicLink' => false, + 'allowCustomPublicShareTokens' => true, 'allowShowToAll' => false, 'creationAllowedGroups' => [ [ @@ -136,6 +138,7 @@ public static function dataGetAppConfig_Default() { 'expected' => [ 'allowPermitAll' => true, 'allowPublicLink' => true, + 'allowCustomPublicShareTokens' => false, 'allowShowToAll' => true, 'creationAllowedGroups' => [], 'restrictCreation' => false,