Skip to content

Commit

Permalink
[wip] feat: Edit share token
Browse files Browse the repository at this point in the history
  • Loading branch information
Pytal committed Nov 15, 2024
1 parent 3e25a62 commit 948ea93
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 35 deletions.
17 changes: 17 additions & 0 deletions apps/files_sharing/lib/Controller/ShareAPIController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -2158,4 +2159,20 @@ 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
*
* @return DataResponse<Http::STATUS_OK, array{token: string}>

Check failure on line 2166 in apps/files_sharing/lib/Controller/ShareAPIController.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

MissingTemplateParam

apps/files_sharing/lib/Controller/ShareAPIController.php:2166:13: MissingTemplateParam: OCP\AppFramework\Http\DataResponse has missing template params, expecting 3 (see https://psalm.dev/182)
*
* 200: Token generated successfully
*/
#[ApiRoute('GET', '/api/v1/token')]

Check failure on line 2170 in apps/files_sharing/lib/Controller/ShareAPIController.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

InvalidDocblock

apps/files_sharing/lib/Controller/ShareAPIController.php:2170:4: InvalidDocblock: Attribute arguments must be named. (see https://psalm.dev/008)
#[NoAdminRequired]
public function getToken(): DataResponse {
$token = $this->shareManager->generateToken();
return new DataResponse([
'token' => $token,
]);
}
}
7 changes: 7 additions & 0 deletions apps/files_sharing/src/models/Share.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
31 changes: 31 additions & 0 deletions apps/files_sharing/src/services/TokenService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import logger from './logger.ts'

interface TokenData {
token: string,
}

export const generateToken = async (): Promise<null | string> => {
try {
const { data } = await axios.get<TokenData>(generateUrl('/api/v1/token'))
return data.token
} catch (error) {
logger.error('Failed to get token from server, falling back to client-side generation', { error })

const chars = 'abcdefgijkmnopqrstwxyzABCDEFGHJKLMNPQRSTWXYZ23456789'
const array = new Uint8Array(10)
const ratio = chars.length / 255
window.crypto.getRandomValues(array)
let token = ''
for (let i = 0; i < array.length; i++) {
token += chars.charAt(array[i] * ratio)
}
return token
}
}
31 changes: 31 additions & 0 deletions apps/files_sharing/src/views/SharingDetailsTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,22 @@
role="region">
<section>
<NcInputField v-if="isPublicShare"
class="sharingTabDetailsView__label"
autocomplete="off"
:label="t('files_sharing', 'Share label')"
:value.sync="share.label" />
<NcInputField v-if="isPublicShare && !isNewShare"
autocomplete="off"
:label="t('files_sharing', 'Link token')"
:helper-text="tokenHelperText"
show-trailing-button
:trailing-button-label="t('files_sharing', 'Generate new token')"
@trailing-button-click="generateNewToken"
:value.sync="share.token">
<template #trailing-button-icon>
<Reload />
</template>
</NcInputField>
<template v-if="isPublicShare">
<NcCheckboxRadioSwitch :checked.sync="isPasswordProtected" :disabled="isPasswordEnforced">
{{ t('files_sharing', 'Set password') }}
Expand Down Expand Up @@ -271,6 +284,7 @@ import UploadIcon from 'vue-material-design-icons/Upload.vue'
import MenuDownIcon from 'vue-material-design-icons/MenuDown.vue'
import MenuUpIcon from 'vue-material-design-icons/MenuUp.vue'
import DotsHorizontalIcon from 'vue-material-design-icons/DotsHorizontal.vue'
import Reload from 'vue-material-design-icons/Reload.vue'

import ExternalShareAction from '../components/ExternalShareAction.vue'

Expand All @@ -279,6 +293,7 @@ import Share from '../models/Share.ts'
import ShareRequests from '../mixins/ShareRequests.js'
import ShareTypes from '../mixins/ShareTypes.js'
import SharesMixin from '../mixins/SharesMixin.js'
import { generateToken } from '../services/TokenService.ts'
import logger from '../services/logger.ts'

import {
Expand Down Expand Up @@ -311,6 +326,7 @@ export default {
MenuDownIcon,
MenuUpIcon,
DotsHorizontalIcon,
Reload,
},
mixins: [ShareTypes, ShareRequests, SharesMixin],
props: {
Expand Down Expand Up @@ -557,6 +573,13 @@ export default {
return t('files_sharing', 'Update share')

},

tokenHelperText() {
return t('files_sharing', 'Set the public link token. Access the share at /s/{token}', {
token: this.share.token || '<token>',
}, undefined, { escape: false, sanitize: false })
},

/**
* Can the sharer set whether the sharee can edit the file ?
*
Expand Down Expand Up @@ -763,6 +786,10 @@ export default {
},

methods: {
generateNewToken() {
this.share.token = generateToken()
},

updateAtomicPermissions({
isReadChecked = this.hasRead,
isEditChecked = this.canEdit,
Expand Down Expand Up @@ -1176,6 +1203,10 @@ export default {
}
}

&__label {
padding-block-end: 6px;
}

&__delete {
>button:first-child {
color: rgb(223, 7, 7);
Expand Down
13 changes: 13 additions & 0 deletions lib/private/Share20/Exception/ShareTokenException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OC\Share20\Exception;

use Exception;

class ShareTokenException extends Exception {
}
76 changes: 41 additions & 35 deletions lib/private/Share20/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use OC\Files\Mount\MoveableMount;
use OC\KnownUser\KnownUserService;
use OC\Share20\Exception\ProviderException;
use OC\Share20\Exception\ShareTokenException;
use OCA\Files_Sharing\AppInfo\Application;
use OCA\Files_Sharing\SharedStorage;
use OCP\EventDispatcher\IEventDispatcher;
Expand Down Expand Up @@ -657,41 +658,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);

Expand Down Expand Up @@ -1992,4 +1959,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;
}
}
9 changes: 9 additions & 0 deletions lib/public/Share/IManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/
namespace OCP\Share;

use OC\Share20\Exception\ShareTokenException;
use OCP\Files\Folder;
use OCP\Files\Node;

Expand Down Expand Up @@ -519,4 +520,12 @@ public function registerShareProvider(string $shareProviderClass): void;
* @since 18.0.0
*/
public function getAllShares(): iterable;

/**
* Generate a unique share token
*
* @throws ShareTokenException Failed to generate a unique token
* @since 31.0.0
*/
public function generateToken(): string;
}

0 comments on commit 948ea93

Please sign in to comment.