diff --git a/.github/workflows/cypress-e2e.yml b/.github/workflows/cypress-e2e.yml index 6d6dabf0fd..a9671d3127 100644 --- a/.github/workflows/cypress-e2e.yml +++ b/.github/workflows/cypress-e2e.yml @@ -58,6 +58,11 @@ jobs: git submodule sync --recursive git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1 + - name: Register main git reference + run: | + main_app_ref="$(if [ "${{ matrix.server-versions }}" = "master" ]; then echo -n "main"; else echo -n "${{ matrix.server-versions }}"; fi)" + echo "main_app_ref=$main_app_ref" >> $GITHUB_ENV + - name: Checkout viewer uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: @@ -65,6 +70,13 @@ jobs: ref: ${{ matrix.server-versions }} path: apps/viewer + - name: Checkout spreed + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + with: + repository: nextcloud/spreed + ref: ${{ env.main_app_ref }} + path: apps/spreed + - name: Checkout files_pdfviewer uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: @@ -106,6 +118,13 @@ jobs: run: | composer install + - name: Build talk + working-directory: apps/spreed + run: | + composer install --no-dev + npm ci + npm run dev + - name: Set up Nextcloud env: DB_PORT: 4444 @@ -125,6 +144,7 @@ jobs: php occ app:enable --force viewer php occ app:enable --force files_pdfviewer php occ app:enable --force richdocuments + php occ app:enable --force spreed php occ app:list php occ config:system:set trusted_domains 1 --value="172.17.0.1" diff --git a/appinfo/info.xml b/appinfo/info.xml index 26092ab34a..21b4a53ec7 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -15,6 +15,7 @@ You can also edit your documents off-line with the Collabora Office app from the agpl Collabora Productivity based on work of Frank Karlitschek, Victor Dubiniuk + diff --git a/cypress/e2e/integration.spec.js b/cypress/e2e/integration.spec.js index 779b12e340..d85039fc3b 100644 --- a/cypress/e2e/integration.spec.js +++ b/cypress/e2e/integration.spec.js @@ -180,4 +180,4 @@ describe('Nextcloud integration', function() { }) }) }) -}) +}) \ No newline at end of file diff --git a/cypress/e2e/talk.spec.js b/cypress/e2e/talk.spec.js new file mode 100644 index 0000000000..64dd25df56 --- /dev/null +++ b/cypress/e2e/talk.spec.js @@ -0,0 +1,110 @@ +/** + * SPDX-FileCopyrightText: 2023 Julius Härtl + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +describe('Talk integraiton integration', function() { + let randUser + + const resetConfig = () => { + cy.nextcloudTestingAppConfigSet('files', 'watermark_enabled', 'no') + cy.nextcloudTestingAppConfigSet('files', 'watermark_text', '{userId}') + cy.nextcloudTestingAppConfigSet('files', 'watermark_shareTalkPublic', 'no') + cy.nextcloudTestingAppConfigSet('richdocuments', 'uiDefaults-UIMode', 'notebookbar') + } + + before(function() { + resetConfig() + cy.createRandomUser().then(user => { + randUser = user + cy.login(user) + cy.uploadFile(user, 'document.odt', 'application/vnd.oasis.opendocument.text', '/document.odt') + }) + }) + + afterEach(() => { + resetConfig() + }) + + const filename = 'document.odt' + + beforeEach(function() { + cy.login(randUser) + }) + + it('Can share a file to a talk room and open it', function() { + cy.createTalkRoom(randUser, { + roomName: 'Test room', + }).then(room => { + cy.log(`Created talk room "${room.name}"`, room) + cy.shareFileToTalkRoom(randUser, filename, room.token) + cy.visit(`/call/${room.token}`) + cy.get('.file-preview') + .should('be.visible') + .should('contain.text', filename) + .click() + + cy.waitForViewer() + cy.waitForCollabora() + }) + }) + + it('See that the file is shared without download', function() { + cy.nextcloudTestingAppConfigSet('files', 'watermark_enabled', 'yes') + cy.nextcloudTestingAppConfigSet('files', 'watermark_shareTalkPublic', 'yes') + cy.nextcloudTestingAppConfigSet('files', 'watermark_text', 'TestingWatermark') + + cy.createTalkRoom(randUser, { + roomName: 'Secure room', + }).then(room => { + cy.log(`Created talk room "${room.name}"`, room) + cy.shareFileToTalkRoom(randUser, filename, room.token, { permission: 1 }) + cy.makeTalkRoomPublic(randUser, room.token) + + cy.logout() + cy.clearAllLocalStorage() + cy.visit(`/call/${room.token}`) + cy.get('.username-form__input input[type="text"]') + .should('be.visible') + .type('Test user{enter}') + + // Assert that the download button is hidden in talk + cy.get('.messages:contains("document.odt")') + .trigger('mouseover') + + cy.get('.file-preview') + .closest('.message') + .find('button[aria-label="Actions"]') + .should('be.visible') + .click() + + cy.get('.action:contains("Download")') + .should('not.exist') + + // Assert the file is still opening + cy.get('.file-preview') + .should('be.visible') + .should('contain.text', filename) + // We need to get the href to work around how cypress works with windows + .invoke('attr', 'href') + .then((href) => { + cy.visit(href) + + cy.get('[data-cy="guestNameModal"]').should('be.visible') + cy.inputCollaboraGuestName('A guest') + + cy.waitForCollabora() + + cy.url().then(url => { + const baseUrl = url.split('?')[0] + cy.request({ + url: baseUrl + '/download', + failOnStatusCode: false, + }).then((response) => { + expect(response.status).to.eq(403) + }) + }) + }) + }) + }) +}) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index dc064b77d6..0ca453c29d 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -82,7 +82,7 @@ Cypress.Commands.add('uploadFile', (user, fixture, mimeType, target = `/${fixtur }) Cypress.Commands.add('ocsRequest', (user, options) => { - const auth = { user: user.userId, password: user.password } + const auth = user ? { user: user.userId, password: user.password } : null return cy.request({ form: true, auth, @@ -109,6 +109,22 @@ Cypress.Commands.add('shareFileToUser', (user, path, targetUser, shareData = {}) }) }) +Cypress.Commands.add('shareFileToTalkRoom', (user, path, roomId, shareData = {}) => { + cy.login(user) + cy.ocsRequest(user, { + method: 'POST', + url: `${url}/ocs/v2.php/apps/files_sharing/api/v1/shares?format=json`, + body: { + path, + shareType: 10, + shareWith: roomId, + ...shareData, + }, + }).then(response => { + cy.log(`${user.userId} shared ${path} with talk room ${roomId}`, response.status) + }) +}) + Cypress.Commands.add('shareFileToRemoteUser', (user, path, targetUser, shareData = {}) => { cy.login(user) const federatedId = `${targetUser.userId}@${url}` @@ -396,10 +412,45 @@ Cypress.Commands.add('verifyTemplateFields', (fields, fileId) => { }) Cypress.Commands.add('pickFile', (filename) => { - cy.get('.office-target-picker') - .find(`tr[data-filename="${filename}"]`) - .click() - cy.get('.office-target-picker') - .find('button[aria-label="Select file"]') - .click() + cy.get('.office-target-picker') + .find(`tr[data-filename="${filename}"]`) + .click() + cy.get('.office-target-picker') + .find('button[aria-label="Select file"]') + .click() }) + +Cypress.Commands.add('createTalkRoom', (user, options = {}) => { + cy.login(user) + return cy.ocsRequest(user, { + method: 'POST', + url: `${url}/ocs/v2.php/apps/spreed/api/v4/room?format=json`, + body: { + roomType: options.roomType || 3, // Default to group conversation + roomName: options.roomName, + invite: options.invite || '', + source: options.source || '', + objectType: options.objectType || '', + objectId: options.objectId || '', + password: options.password || '', + } + }).then(response => { + cy.log(`Created talk room "${options.roomName}"`, response.status) + return cy.wrap(response.body.ocs.data) + }) +}) + +Cypress.Commands.add('makeTalkRoomPublic', (user, token, password = '') => { + cy.login(user) + return cy.ocsRequest(user, { + method: 'POST', + url: `${url}/ocs/v2.php/apps/spreed/api/v4/room/${token}/public?format=json`, + body: { + password: password, + } + }).then(response => { + cy.log(`Made talk room public`, response.status) + return cy.wrap(response.body.ocs.data) + }) +}) + diff --git a/lib/AppConfig.php b/lib/AppConfig.php index 69dbd864ed..8b24ff48f3 100644 --- a/lib/AppConfig.php +++ b/lib/AppConfig.php @@ -204,6 +204,13 @@ public function useSecureViewAdditionalMimes(): bool { return $this->config->getAppValue(Application::APPNAME, self::USE_SECURE_VIEW_ADDITIONAL_MIMES, 'no') === 'yes'; } + public function getMimeTypes(): array { + return array_merge( + Capabilities::MIMETYPES, + Capabilities::MIMETYPES_MSOFFICE, + ); + } + public function getDomainList(): array { $urls = array_merge( [ $this->domainOnly($this->getCollaboraUrlPublic()) ], diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index a619877802..2d8150a844 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -9,6 +9,7 @@ namespace OCA\Richdocuments\AppInfo; use OCA\Files_Sharing\Event\ShareLinkAccessedEvent; +use OCA\Richdocuments\AppConfig; use OCA\Richdocuments\Capabilities; use OCA\Richdocuments\Conversion\ConversionProvider; use OCA\Richdocuments\Db\WopiMapper; @@ -20,6 +21,7 @@ use OCA\Richdocuments\Listener\FileCreatedFromTemplateListener; use OCA\Richdocuments\Listener\LoadAdditionalListener; use OCA\Richdocuments\Listener\LoadViewerListener; +use OCA\Richdocuments\Listener\OverwritePublicSharePropertiesListener; use OCA\Richdocuments\Listener\ReferenceListener; use OCA\Richdocuments\Listener\RegisterTemplateFileCreatorListener; use OCA\Richdocuments\Listener\ShareLinkListener; @@ -32,6 +34,7 @@ use OCA\Richdocuments\Preview\OpenDocument; use OCA\Richdocuments\Preview\Pdf; use OCA\Richdocuments\Reference\OfficeTargetReferenceProvider; +use OCA\Richdocuments\Storage\SecureViewWrapper; use OCA\Richdocuments\TaskProcessing\SlideDeckGenerationProvider; use OCA\Richdocuments\TaskProcessing\SlideDeckGenerationTaskType; use OCA\Richdocuments\TaskProcessing\TextToDocumentProvider; @@ -39,6 +42,7 @@ use OCA\Richdocuments\TaskProcessing\TextToSpreadsheetProvider; use OCA\Richdocuments\TaskProcessing\TextToSpreadsheetTaskType; use OCA\Richdocuments\Template\CollaboraTemplateProvider; +use OCA\Talk\Events\OverwritePublicSharePropertiesEvent; use OCA\Viewer\Event\LoadViewer; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; @@ -47,12 +51,15 @@ use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; use OCP\Collaboration\Reference\RenderReferenceEvent; use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent; +use OCP\Files\Storage\IStorage; use OCP\Files\Template\BeforeGetTemplatesEvent; use OCP\Files\Template\FileCreatedFromTemplateEvent; use OCP\Files\Template\RegisterTemplateCreatorEvent; +use OCP\IAppConfig; use OCP\Preview\BeforePreviewFetchedEvent; use OCP\Security\CSP\AddContentSecurityPolicyEvent; use OCP\Security\FeaturePolicy\AddFeaturePolicyEvent; +use OCP\Server; class Application extends App implements IBootstrap { public const APPNAME = 'richdocuments'; @@ -62,6 +69,8 @@ public function __construct(array $urlParams = []) { } public function register(IRegistrationContext $context): void { + \OCP\Util::connectHook('OC_Filesystem', 'preSetup', $this, 'addStorageWrapper'); + $context->registerTemplateProvider(CollaboraTemplateProvider::class); $context->registerCapability(Capabilities::class); $context->registerMiddleWare(WOPIMiddleware::class); @@ -76,6 +85,7 @@ public function register(IRegistrationContext $context): void { $context->registerEventListener(RenderReferenceEvent::class, ReferenceListener::class); $context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class); $context->registerEventListener(BeforeGetTemplatesEvent::class, BeforeGetTemplatesListener::class); + $context->registerEventListener(OverwritePublicSharePropertiesEvent::class, OverwritePublicSharePropertiesListener::class); $context->registerReferenceProvider(OfficeTargetReferenceProvider::class); $context->registerSensitiveMethods(WopiMapper::class, [ 'getPathForToken', @@ -101,4 +111,32 @@ public function register(IRegistrationContext $context): void { public function boot(IBootContext $context): void { } + + /** + * @internal + */ + public function addStorageWrapper(): void { + if (Server::get(IAppConfig::class)->getValueString(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_enabled', 'no') === 'no') { + return; + } + + \OC\Files\Filesystem::addStorageWrapper('richdocuments', [$this, 'addStorageWrapperCallback'], -10); + } + + /** + * @param $mountPoint + * @param IStorage $storage + * @return SecureViewWrapper|IStorage + *@internal + */ + public function addStorageWrapperCallback($mountPoint, IStorage $storage) { + if (!\OC::$CLI && $mountPoint !== '/') { + return new SecureViewWrapper([ + 'storage' => $storage, + 'mountPoint' => $mountPoint, + ]); + } + + return $storage; + } } diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index 4f85df20d0..9ac94a2bc1 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -213,6 +213,7 @@ public function updateWatermarkSettings($settings = []): JSONResponse { 'watermark_shareAll', 'watermark_shareRead', 'watermark_shareDisabledDownload', + 'watermark_shareTalkPublic', 'watermark_linkSecure', 'watermark_linkRead', 'watermark_linkAll', diff --git a/lib/Controller/WopiController.php b/lib/Controller/WopiController.php index 4c209744e5..dbb8b62288 100644 --- a/lib/Controller/WopiController.php +++ b/lib/Controller/WopiController.php @@ -135,6 +135,9 @@ public function checkFileInfo(string $fileId, string $access_token): JSONRespons $isSmartPickerEnabled = (bool)$wopi->getCanwrite() && !$isPublic && !$wopi->getDirect(); $isTaskProcessingEnabled = $isSmartPickerEnabled && $this->taskProcessingManager->isTaskProcessingEnabled(); + $share = $this->getShareForWopiToken($wopi, $file); + $shouldUseSecureView = $this->permissionManager->shouldWatermark($file, $wopi->getEditorUid(), $share); + // If the file is locked manually by a user we want to open it read only for all others $canWriteThroughLock = true; try { @@ -156,7 +159,7 @@ public function checkFileInfo(string $fileId, string $access_token): JSONRespons 'UserExtraInfo' => [], 'UserPrivateInfo' => [], 'UserCanWrite' => $canWriteThroughLock && (bool)$wopi->getCanwrite(), - 'UserCanNotWriteRelative' => $isPublic || $this->encryptionManager->isEnabled() || $wopi->getHideDownload() || $wopi->isRemoteToken(), + 'UserCanNotWriteRelative' => $isPublic || $this->encryptionManager->isEnabled() || $wopi->getHideDownload() || $wopi->isRemoteToken() || $shouldUseSecureView, 'PostMessageOrigin' => $wopi->getServerHost(), 'LastModifiedTime' => Helper::toISO8601($file->getMTime()), 'SupportsRename' => !$isVersion && !$wopi->isRemoteToken(), @@ -166,11 +169,11 @@ public function checkFileInfo(string $fileId, string $access_token): JSONRespons 'EnableShare' => $file->isShareable() && !$isVersion && !$isPublic, 'HideUserList' => '', 'EnableOwnerTermination' => $wopi->getCanwrite() && !$isPublic, - 'DisablePrint' => $wopi->getHideDownload(), - 'DisableExport' => $wopi->getHideDownload(), - 'DisableCopy' => $wopi->getHideDownload(), - 'HideExportOption' => $wopi->getHideDownload(), - 'HidePrintOption' => $wopi->getHideDownload(), + 'DisablePrint' => $wopi->getHideDownload() || $shouldUseSecureView, + 'DisableExport' => $wopi->getHideDownload() || $shouldUseSecureView, + 'DisableCopy' => $wopi->getHideDownload() || $shouldUseSecureView, + 'HideExportOption' => $wopi->getHideDownload() || $shouldUseSecureView, + 'HidePrintOption' => $wopi->getHideDownload() || $shouldUseSecureView, 'DownloadAsPostMessage' => $wopi->getDirect(), 'SupportsLocks' => $this->lockManager->isLockProviderAvailable(), 'IsUserLocked' => $this->permissionManager->userIsFeatureLocked($wopi->getEditorUid()), @@ -223,8 +226,7 @@ public function checkFileInfo(string $fileId, string $access_token): JSONRespons $response['TemplateSource'] = $this->getWopiUrlForTemplate($wopi); } - $share = $this->getShareForWopiToken($wopi, $file); - if ($this->permissionManager->shouldWatermark($file, $wopi->getEditorUid(), $share)) { + if ($shouldUseSecureView) { $email = $user !== null && !$isPublic ? $user->getEMailAddress() : ''; $currentDateTime = new \DateTime( 'now', diff --git a/lib/Listener/OverwritePublicSharePropertiesListener.php b/lib/Listener/OverwritePublicSharePropertiesListener.php new file mode 100644 index 0000000000..fadfa49817 --- /dev/null +++ b/lib/Listener/OverwritePublicSharePropertiesListener.php @@ -0,0 +1,40 @@ + */ +class OverwritePublicSharePropertiesListener implements IEventListener { + public function __construct( + private PermissionManager $permissionManager, + private ?string $userId, + ) { + } + + public function handle(Event $event): void { + if (!$event instanceof OverwritePublicSharePropertiesEvent) { + return; + } + + $share = $event->getShare(); + try { + $node = $share->getNode(); + } catch (NotFoundException) { + return; + } + + if ($this->permissionManager->shouldWatermark($node, $this->userId, $share)) { + $share->setHideDownload(true); + } + } +} diff --git a/lib/Middleware/WOPIMiddleware.php b/lib/Middleware/WOPIMiddleware.php index 613c9e7db4..3efe6afcc5 100644 --- a/lib/Middleware/WOPIMiddleware.php +++ b/lib/Middleware/WOPIMiddleware.php @@ -33,6 +33,7 @@ public function __construct( private IRequest $request, private WopiMapper $wopiMapper, private LoggerInterface $logger, + private bool $isWOPIRequest = false, ) { } @@ -78,6 +79,8 @@ public function beforeController($controller, $methodName) { $this->logger->error('Failed to validate WOPI access', [ 'exception' => $e ]); throw new NotPermittedException(); } + + $this->isWOPIRequest = true; } public function afterException($controller, $methodName, \Exception $exception): Response { @@ -110,4 +113,8 @@ public function isWOPIAllowed(): bool { $this->logger->warning('WOPI request denied from ' . $userIp . ' as it does not match the configured ranges: ' . implode(', ', $allowedRanges)); return false; } + + public function isWOPIRequest(): bool { + return $this->isWOPIRequest; + } } diff --git a/lib/PermissionManager.php b/lib/PermissionManager.php index 49f0a79eaa..5e081c4147 100644 --- a/lib/PermissionManager.php +++ b/lib/PermissionManager.php @@ -117,6 +117,10 @@ public function shouldWatermark(Node $node, ?string $userId = null, ?IShare $sha return false; } + if (!in_array($node->getMimetype(), $this->appConfig->getMimeTypes(), true)) { + return false; + } + $fileId = $node->getId(); $isUpdatable = $node->isUpdateable() && (!$share || $share->getPermissions() & Constants::PERMISSION_UPDATE); @@ -162,6 +166,12 @@ public function shouldWatermark(Node $node, ?string $userId = null, ?IShare $sha return true; } + if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_shareTalkPublic', 'no') === 'yes') { + if ($userId === null && $share?->getShareType() === IShare::TYPE_ROOM) { + return true; + } + } + if ($userId !== null && $this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_allGroups', 'no') === 'yes') { $groups = $this->appConfig->getAppValueArray('watermark_allGroupsList'); foreach ($groups as $group) { diff --git a/lib/Storage/SecureViewWrapper.php b/lib/Storage/SecureViewWrapper.php new file mode 100644 index 0000000000..383882f342 --- /dev/null +++ b/lib/Storage/SecureViewWrapper.php @@ -0,0 +1,109 @@ +permissionManager = Server::get(PermissionManager::class); + $this->wopiMiddleware = Server::get(WOPIMiddleware::class); + $this->rootFolder = Server::get(IRootFolder::class); + $this->userSession = Server::get(IUserSession::class); + + $this->mountPoint = $parameters['mountPoint']; + } + + public function fopen($path, $mode) { + $this->checkFileAccess($path); + + return $this->storage->fopen($path, $mode); + } + + public function file_get_contents(string $path): false|string { + $this->checkFileAccess($path); + + return $this->storage->file_get_contents($path); + } + + public function copy(string $source, string $target): bool { + $this->checkSourceAndTarget($source, $target); + + return parent::copy($source, $target); + } + + public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { + $this->checkSourceAndTarget($sourceInternalPath, $targetInternalPath, $sourceStorage); + + return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); + } + + public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { + $this->checkSourceAndTarget($sourceInternalPath, $targetInternalPath, $sourceStorage); + + return parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); + } + + public function rename(string $source, string $target): bool { + $this->checkSourceAndTarget($source, $target); + + return parent::rename($source, $target); + } + + /** + * @throws ForbiddenException + */ + private function checkFileAccess(string $path): void { + if ($this->shouldSecure($path) && !$this->wopiMiddleware->isWOPIRequest()) { + throw new ForbiddenException('Download blocked due the secure view policy', false); + } + } + + private function shouldSecure(string $path, ?IStorage $sourceStorage = null): bool { + if ($sourceStorage !== $this && $sourceStorage !== null) { + $fp = $sourceStorage->fopen($path, 'r'); + fclose($fp); + } + + $storage = $sourceStorage ?? $this; + + $isSharedStorage = $storage->instanceOfStorage(ISharedStorage::class); + $mountNode = $this->rootFolder->get($storage->getMountPoint()); + $node = $mountNode instanceof Folder ? $mountNode->get($path) : $mountNode; + $share = $isSharedStorage ? $node->getStorage()->getShare() : null; + $userId = $this->userSession->getUser()?->getUID(); + + return $this->permissionManager->shouldWatermark($node, $userId, $share); + } + + + private function checkSourceAndTarget(string $source, string $target, ?IStorage $sourceStorage = null): void { + if ($this->shouldSecure($source, $sourceStorage) && !$this->shouldSecure($target)) { + throw new ForbiddenException('Download blocked due the secure view policy. The source requires secure view that the target cannot offer.', false); + } + } +} diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue index fb6ca9e4b9..492ea11d98 100644 --- a/src/components/AdminSettings.vue +++ b/src/components/AdminSettings.vue @@ -310,16 +310,16 @@

{{ t('richdocuments', 'Secure View') }}

-

{{ t('richdocuments', 'Secure view enables you to secure documents by embedding a watermark') }}

+

{{ t('richdocuments', 'Secure view enables you to secure office documents by blocking downloads, previews and showing a watermark') }}

  • {{ t('richdocuments', 'The settings only apply to compatible office files that are opened in Nextcloud Office') }}
  • +
  • {{ t('richdocuments', 'Downloading the file through WebDAV will be blocked') }}
  • {{ t('richdocuments', 'The following options within Nextcloud Office will be disabled: Copy, Download, Export, Print') }}
  • -
  • {{ t('richdocuments', 'Files may still be downloadable through Nextcloud unless restricted otherwise through sharing or access control settings') }}
  • {{ t('richdocuments', 'Files may still be downloadable via WOPI requests if WOPI settings are not correctly configured') }}
  • -
  • {{ t('richdocuments', 'Previews will be blocked for watermarked files to not leak the first page of documents') }}
  • +
  • {{ t('richdocuments', 'Previews will be blocked') }}
@@ -331,62 +331,67 @@ @update="update" />

+

Link shares

- +

@@ -512,6 +517,7 @@ export default { enabled: false, shareAll: false, shareRead: false, + shareTalkPublic: true, linkSecure: false, linkRead: false, linkAll: false, diff --git a/src/components/SettingsCheckbox.vue b/src/components/SettingsCheckbox.vue index 6266dca574..c1aee937f4 100644 --- a/src/components/SettingsCheckbox.vue +++ b/src/components/SettingsCheckbox.vue @@ -5,14 +5,12 @@ diff --git a/src/components/SettingsInputText.vue b/src/components/SettingsInputText.vue index ef39e3269c..19b42e8473 100644 --- a/src/components/SettingsInputText.vue +++ b/src/components/SettingsInputText.vue @@ -6,26 +6,29 @@