From 150628d6ed3fdf83c4060d05b14a0138d5caeab8 Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Thu, 4 Apr 2024 09:58:51 -0400 Subject: [PATCH] fix: guest name picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Elizabeth Danzberger Signed-off-by: Julius Härtl --- cypress/e2e/share-link.js | 79 ++++++++++++---- cypress/support/commands.js | 25 ++++- lib/Controller/DocumentController.php | 8 +- lib/Listener/ShareLinkListener.php | 5 +- lib/PermissionManager.php | 20 +++- lib/Service/InitialStateService.php | 4 + lib/TokenManager.php | 1 + src/components/GuestNamePicker.vue | 131 ++++++++++++++++++++++++++ src/document.js | 48 +--------- src/helpers/getLoggedInUser.js | 34 +++++++ src/helpers/guestName.js | 50 +++------- src/helpers/isDocument.js | 1 + src/helpers/isPublicPage.js | 1 + src/view/Office.vue | 23 ++++- 14 files changed, 324 insertions(+), 106 deletions(-) create mode 100644 src/components/GuestNamePicker.vue create mode 100644 src/helpers/getLoggedInUser.js diff --git a/cypress/e2e/share-link.js b/cypress/e2e/share-link.js index 8ba21a143f..84f49a03f8 100644 --- a/cypress/e2e/share-link.js +++ b/cypress/e2e/share-link.js @@ -34,18 +34,17 @@ describe('Public sharing of office documents', function() { cy.uploadFile(shareOwner, 'document.odt', 'application/vnd.oasis.opendocument.text', '/my-share/document.odt') }) - const matrix = [shareOwner, otherUser, null] + const userMatrix = [shareOwner, otherUser] + const guestName = randHash() describe('Open a shared file', function() { - for (const index in matrix) { - const viewingUser = matrix[index] + for (const index in userMatrix) { + const viewingUser = userMatrix[index] + it('Loads file as user: ' + viewingUser?.userId, () => { cy.shareLink(shareOwner, '/document.odt').then((token) => { - if (viewingUser !== null) { - cy.login(viewingUser) - } else { - cy.logout() - } + cy.login(viewingUser) + cy.visit(`/s/${token}`, { onBeforeLoad(win) { cy.spy(win, 'postMessage').as('postMessage') @@ -66,21 +65,40 @@ describe('Public sharing of office documents', function() { cy.get('@loleafletframe').within(() => { cy.get('#closebutton').click() }) + cy.get('#viewer', { timeout: 5000 }).should('not.exist') }) }) } + + it('Loads file as guest: ' + guestName, () => { + cy.shareLink(shareOwner, '/document.odt').then((token) => { + cy.logout() + + cy.visit(`/s/${token}`, { + onBeforeLoad(win) { + cy.spy(win, 'postMessage').as('postMessage') + }, + }) + + cy.inputCollaboraGuestName(guestName) + cy.waitForCollabora() + cy.waitForPostMessage('App_LoadingStatus', { Status: 'Document_Loaded' }) + + cy.get('@loleafletframe').within(() => { + cy.get('#closebutton').click() + }) + + cy.get('#viewer', { timeout: 5000 }).should('not.exist') + }) + }) }) describe('Open a file in a shared folder', function() { - for (const index in matrix) { - const viewingUser = matrix[index] + for (const index in userMatrix) { + const viewingUser = userMatrix[index] it('Loads file in shared folder as user: ' + viewingUser?.userId, () => { - if (viewingUser !== null) { - cy.login(viewingUser) - } else { - cy.logout() - } + cy.login(viewingUser) cy.shareLink(shareOwner, '/my-share').then((token) => { cy.visit(`/s/${token}`, { @@ -88,16 +106,45 @@ describe('Public sharing of office documents', function() { cy.spy(win, 'postMessage').as('postMessage') }, }) + cy.get('tr[data-file="document.odt"] a.name').click() + cy.waitForViewer() cy.waitForCollabora() + cy.waitForPostMessage('App_LoadingStatus', { Status: 'Document_Loaded' }) + cy.get('@loleafletframe').within(() => { cy.get('#closebutton').click() }) + cy.get('#viewer', { timeout: 5000 }).should('not.exist') }) }) - } + + it('Loads file in shared folder as guest: ' + guestName, () => { + cy.shareLink(shareOwner, '/my-share').then((token) => { + cy.logout() + + cy.visit(`/s/${token}`, { + onBeforeLoad(win) { + cy.spy(win, 'postMessage').as('postMessage') + }, + }) + + cy.get('tr[data-file="document.odt"] a.name').click() + + cy.inputCollaboraGuestName(guestName) + cy.waitForViewer() + cy.waitForCollabora() + cy.waitForPostMessage('App_LoadingStatus', { Status: 'Document_Loaded' }) + + cy.get('@loleafletframe').within(() => { + cy.get('#closebutton').click() + }) + + cy.get('#viewer', { timeout: 5000 }).should('not.exist') + }) + }) }) }) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 1538e23daf..72de16fabe 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -204,6 +204,7 @@ Cypress.Commands.add('waitForViewer', () => { .and('have.class', 'modal-mask') .and('not.have.class', 'icon-loading') }) + Cypress.Commands.add('waitForCollabora', (wrapped = false) => { if (wrapped) { cy.get('[data-cy="documentframe"]', { timeout: 30000 }) @@ -224,6 +225,28 @@ Cypress.Commands.add('waitForCollabora', (wrapped = false) => { cy.get('@loleafletframe').find('#main-document-content').should('be.visible') }) +Cypress.Commands.add('waitForPostMessage', (messageId, values = undefined) => { + cy.get('@postMessage', { timeout: 20000 }).should(spy => { + const calls = spy.getCalls() + const findMatchingCall = calls.find(call => call.args[0].indexOf('"MessageId":"' + messageId + '"') !== -1) + if (!findMatchingCall) { + return expect(findMatchingCall).to.not.be.undefined + } + if (!values) { + const object = JSON.parse(findMatchingCall.args[0]) + values.forEach(value => { + expect(object.Values).to.have.property(value, values[value]) + }) + } + }) +}) + +Cypress.Commands.add('inputCollaboraGuestName', (guestName = 'cloud') => { + cy.get('[data-cy="guestNameModal"]').should('be.visible') + cy.get('[data-cy="guestNameInput"]').type(guestName) + cy.get('[data-cy="guestNameSubmit"]').click() +}) + Cypress.Commands.add('uploadSystemTemplate', () => { cy.login(new User('admin', 'admin')) cy.visit('/settings/admin/richdocuments') @@ -234,4 +257,4 @@ Cypress.Commands.add('uploadSystemTemplate', () => { mimeType: 'application/vnd.oasis.opendocument.presentation-template', }, { force: true }) cy.get('#richdocuments-templates li').contains('systemtemplate.otp') -}) \ No newline at end of file +}) diff --git a/lib/Controller/DocumentController.php b/lib/Controller/DocumentController.php index c2b02374be..23d82754b3 100644 --- a/lib/Controller/DocumentController.php +++ b/lib/Controller/DocumentController.php @@ -376,11 +376,15 @@ public function editOnlineTarget(int $fileId, ?string $target = null): RedirectR } #[PublicPage] - public function token(int $fileId, ?string $shareToken = null, ?string $path = null): DataResponse { + public function token(int $fileId, ?string $shareToken = null, ?string $path = null, ?string $guestName = null): DataResponse { try { $share = $shareToken ? $this->shareManager->getShareByToken($shareToken) : null; $file = $shareToken ? $this->getFileForShare($share, $fileId, $path) : $this->getFileForUser($fileId, $path); + if ($this->userId === null) { + $this->userId = $guestName; + } + $wopi = $this->getToken($file, $share); return new DataResponse(array_merge( @@ -471,7 +475,7 @@ private function getToken(File $file, ?IShare $share = null, int $version = null return $this->tokenManager->generateWopiTokenForTemplate($templateFile, $share?->getShareOwner() ?? $this->userId, $file->getId()); } - return $this->tokenManager->generateWopiToken($this->getWopiFileId($file->getId(), $version), $share?->getToken(), $this->userId); + return $this->tokenManager->generateWopiToken($this->getWopiFileId($file->getId(), $version), $share?->getToken(), $this->userId); } private function getWopiFileId(int $fileId, int $version = null): string { diff --git a/lib/Listener/ShareLinkListener.php b/lib/Listener/ShareLinkListener.php index 5ac687b255..facdf5593f 100644 --- a/lib/Listener/ShareLinkListener.php +++ b/lib/Listener/ShareLinkListener.php @@ -56,12 +56,15 @@ public function handle(Event $event): void { /** @var IShare $share */ $share = $event->getShare(); $owner = $share->getShareOwner(); + $loggedInUser = $this->permissionManager->loggedInUser(); if ($this->permissionManager->isEnabledForUser($owner)) { + $this->initialStateService->prepareParams(['userId' => $loggedInUser]); $this->initialStateService->provideCapabilities(); + Util::addScript('richdocuments', 'richdocuments-files'); Util::addScript('richdocuments', 'richdocuments-viewer', 'viewer'); - Util::addScript('richdocuments', 'richdocuments-public'); + Util::addScript('richdocuments', 'richdocuments-public', 'viewer'); } } } diff --git a/lib/PermissionManager.php b/lib/PermissionManager.php index fd77a42d8e..7badc79d52 100644 --- a/lib/PermissionManager.php +++ b/lib/PermissionManager.php @@ -67,7 +67,7 @@ private function userMatchesGroupList(?string $userId = null, ?array $groupList $incognito = true; } $user = $this->userSession->getUser(); - $userId = $user ? $user->getUID() : null; + $userId = $user?->getUID(); if ($incognito) { \OC_User::setIncognitoMode(true); } @@ -98,6 +98,24 @@ private function userMatchesGroupList(?string $userId = null, ?array $groupList return false; } + public function loggedInUser(): ?string { + $incognito = false; + + if (\OC_User::isIncognitoMode()) { + \OC_User::setIncognitoMode(false); + $incognito = true; + } + + $user = $this->userSession->getUser(); + $userId = $user?->getUID(); + + if ($incognito) { + \OC_User::setIncognitoMode(true); + } + + return $userId; + } + public function isEnabledForUser(string $userId = null): bool { if ($this->userMatchesGroupList($userId, $this->appConfig->getUseGroups())) { return true; diff --git a/lib/Service/InitialStateService.php b/lib/Service/InitialStateService.php index 6726a09dcf..e9d2a78b7c 100644 --- a/lib/Service/InitialStateService.php +++ b/lib/Service/InitialStateService.php @@ -94,14 +94,18 @@ public function prepareParams(array $params): array { } private function provideOptions(): void { + $this->initialState->provideInitialState('loggedInUser', $this->userId ?? false); + $this->initialState->provideInitialState('theme', $this->config->getAppValue(Application::APPNAME, 'theme', 'nextcloud')); $this->initialState->provideInitialState('uiDefaults', [ 'UIMode' => $this->config->getAppValue(Application::APPNAME, 'uiDefaults-UIMode', 'notebookbar') ]); + $logoSet = $this->config->getAppValue('theming', 'logoheaderMime', '') !== ''; if (!$logoSet) { $logoSet = $this->config->getAppValue('theming', 'logoMime', '') !== ''; } + $this->initialState->provideInitialState('theming-customLogo', ($logoSet ? $this->urlGenerator->getAbsoluteURL($this->themingDefaults->getLogo()) : false)); diff --git a/lib/TokenManager.php b/lib/TokenManager.php index b1128953c2..4621e9ec3f 100644 --- a/lib/TokenManager.php +++ b/lib/TokenManager.php @@ -96,6 +96,7 @@ public function generateWopiToken(string $fileId, ?string $shareToken = null, ?s $owneruid = null; $hideDownload = false; $rootFolder = $this->rootFolder; + // if the user is not logged-in do use the sharers storage if ($shareToken !== null) { /** @var File $file */ diff --git a/src/components/GuestNamePicker.vue b/src/components/GuestNamePicker.vue new file mode 100644 index 0000000000..333a13a546 --- /dev/null +++ b/src/components/GuestNamePicker.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/src/document.js b/src/document.js index 7c5659f900..69a127878f 100644 --- a/src/document.js +++ b/src/document.js @@ -4,10 +4,8 @@ import { generateOcsUrl, getRootUrl, imagePath } from '@nextcloud/router' import { getRequestToken } from '@nextcloud/auth' import { loadState } from '@nextcloud/initial-state' import Config from './services/config.tsx' -import { setGuestName, shouldAskForGuestName } from './helpers/guestName.js' import { getUIDefaults, generateCSSVarTokens, getCollaboraTheme } from './helpers/coolParameters.js' import { enableScrollLock } from './helpers/safariFixer.js' - import PostMessageService from './services/postMessage.tsx' import { callMobileMessage, @@ -15,7 +13,6 @@ import { isMobileInterfaceAvailable, } from './helpers/mobile.js' import { getWopiUrl, getSearchParam, getNextcloudUrl } from './helpers/url.js' - import '../css/document.scss' import axios from '@nextcloud/axios' @@ -92,42 +89,6 @@ const hideLoadingIndicator = () => { showLoadingIndicator() -$.widget('oc.guestNamePicker', { - _create() { - hideLoadingIndicator() - - const text = document.createElement('div') - text.setAttribute('style', 'margin: 0 auto; margin-top: 100px; text-align: center;') - text.innerHTML = t('richdocuments', 'Please choose your nickname to continue as guest user.') - - const div = document.createElement('div') - div.setAttribute('style', 'margin: 0 auto; width: 250px; display: flex;') - const nick = '' - const btn = '' - div.innerHTML = nick + btn - - $('#documents-content').prepend(div) - $('#documents-content').prepend(text) - const setGuestNameSubmit = () => { - const username = $('#nickname').val() - div.remove() - text.innerText = '' - text.classList.add('icon-loading') - setGuestName(username).then(() => { - $('#documents-content').remove() - documentsMain.initSession() - }) - } - - $('#nickname').keyup(function(event) { - if (event.which === 13) { - setGuestNameSubmit() - } - }) - $('#btn').click(() => setGuestNameSubmit()) - }, -}) - /** * Type definitions for WOPI Post message objects * @@ -785,7 +746,7 @@ const documentsMain = { }, } -$(document).ready(function() { +document.addEventListener('DOMContentLoaded', async () => { if (!OCA.RichDocuments) { OCA.RichDocuments = {} @@ -799,12 +760,7 @@ $(document).ready(function() { Config.update('wopi_callback_url', loadState('richdocuments', 'wopi_callback_url', '')) - if (shouldAskForGuestName()) { - PostMessages.sendPostMessage('parent', 'NC_ShowNamePicker') - $('#documents-content').guestNamePicker() - } else { - documentsMain.initSession() - } + documentsMain.initSession() documentsMain.renderComplete = true const viewport = document.querySelector('meta[name=viewport]') diff --git a/src/helpers/getLoggedInUser.js b/src/helpers/getLoggedInUser.js new file mode 100644 index 0000000000..27b97a0c12 --- /dev/null +++ b/src/helpers/getLoggedInUser.js @@ -0,0 +1,34 @@ +/** + * @copyright Copyright (c) 2024 Elizabeth Danzberger + * + * @author Elizabeth Danzberger + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { loadState } from '@nextcloud/initial-state' + +/** + * Gets the current user's display name if logged in. + * + * @return boolean | string + */ +function getLoggedInUser() { + return loadState('richdocuments', 'loggedInUser') +} + +export default getLoggedInUser diff --git a/src/helpers/guestName.js b/src/helpers/guestName.js index fd650e8fce..a293c74409 100644 --- a/src/helpers/guestName.js +++ b/src/helpers/guestName.js @@ -20,54 +20,32 @@ * */ -import Config from './../services/config.tsx' import { getCurrentUser } from '@nextcloud/auth' -import axios from '@nextcloud/axios' -import { generateOcsUrl } from '@nextcloud/router' -import mobile from './mobile.js' +import getLoggedInUser from '../helpers/getLoggedInUser.js' -let guestName = '' - -const getGuestNameCookie = function() { - if (guestName === '') { - const name = 'guestUser=' - const matchedCookie = document.cookie.split(';') - .map((cookie) => { - try { - return decodeURIComponent(cookie.trim()) - } catch (e) { - return cookie.trim() - } - }).find((cookie) => { - return cookie.indexOf(name) === 0 - }) - guestName = matchedCookie ? matchedCookie.substring(name.length) : '' - } - return guestName +const cookieAlreadySet = (cookieName) => { + return document.cookie + .split(';') + .some(cookie => { + return cookie.trim().startsWith(`${cookieName}=`) + }) } -const setGuestName = function(username) { +const setGuestNameCookie = (username) => { if (username !== '') { document.cookie = 'guestUser=' + encodeURIComponent(username) + '; path=/' - guestName = username } - const accessToken = encodeURIComponent(Config.get('token')) - return axios.post(generateOcsUrl('apps/richdocuments/api/v1/wopi/guestname', 2), { - access_token: accessToken, - guestName, - }) } const shouldAskForGuestName = () => { - return (!mobile.isDirectEditing() || Config.get('directGuest')) - && (!getCurrentUser() || getCurrentUser()?.uid === '') - && !Config.get('userId') - && getGuestNameCookie() === '' - && (Config.get('permissions') & OC.PERMISSION_UPDATE) + const noLoggedInUser = !getLoggedInUser() + const noGuestCookie = !cookieAlreadySet('guestUser') + const noCurrentUser = !getCurrentUser() || getCurrentUser()?.uid === '' + + return noLoggedInUser && noGuestCookie && noCurrentUser } export { - getGuestNameCookie, - setGuestName, + setGuestNameCookie, shouldAskForGuestName, } diff --git a/src/helpers/isDocument.js b/src/helpers/isDocument.js index a9814bbe18..7ee8b6ff4c 100644 --- a/src/helpers/isDocument.js +++ b/src/helpers/isDocument.js @@ -5,6 +5,7 @@ const mimetypes = getCapabilities().richdocuments.mimetypes /** * Determines if the mimetype of the resource is supported by richdocuments + * * @return {boolean} */ function isDocument() { diff --git a/src/helpers/isPublicPage.js b/src/helpers/isPublicPage.js index 1aead4ba7d..06b6feefc7 100644 --- a/src/helpers/isPublicPage.js +++ b/src/helpers/isPublicPage.js @@ -1,5 +1,6 @@ /** * Determines if the resource is a public share + * * @return {boolean} */ function isPublic() { diff --git a/src/view/Office.vue b/src/view/Office.vue index 16635423a8..de409cbaf6 100644 --- a/src/view/Office.vue +++ b/src/view/Office.vue @@ -91,7 +91,7 @@ import { NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue' import AlertOctagonOutline from 'vue-material-design-icons/AlertOctagonOutline.vue' import { loadState } from '@nextcloud/initial-state' -import { showInfo } from '@nextcloud/dialogs' +import { showInfo, spawnDialog } from '@nextcloud/dialogs' import ZoteroHint from '../components/Modal/ZoteroHint.vue' import { basename, dirname } from 'path' @@ -123,6 +123,7 @@ import saveAs from '../mixins/saveAs.js' import uiMention from '../mixins/uiMention.js' import version from '../mixins/version.js' import { getCurrentUser } from '@nextcloud/auth' +import { shouldAskForGuestName } from '../helpers/guestName.js' const FRAME_DOCUMENT = 'FRAME_DOCUMENT' @@ -175,6 +176,8 @@ export default { errorType: null, loadingMsg: null, + guestName: null, + showLinkPicker: false, showZotero: false, modified: false, @@ -259,7 +262,21 @@ export default { } this.postMessage.registerPostMessageHandler(this.postMessageHandler) - await this.load() + if (shouldAskForGuestName()) { + const { default: GuestNamePicker } = await import( + /* webpackChunkName: 'GuestNamePicker' */ + '../components/GuestNamePicker.vue') + + spawnDialog(GuestNamePicker, { + fileName: basename(this.filename), + onSubmit: async (guestName) => { + this.guestName = guestName + await this.load() + }, + }) + } else { + await this.load() + } }, beforeDestroy() { this.postMessage.unregisterPostMessageHandler(this.postMessageHandler) @@ -273,7 +290,7 @@ export default { // Generate WOPI token const { data } = await axios.post(generateUrl('/apps/richdocuments/token'), { - fileId: fileid, shareToken: this.shareToken, version, + fileId: fileid, shareToken: this.shareToken, version, guestName: this.guestName, }) Config.update('urlsrc', data.urlSrc) Config.update('wopi_callback_url', loadState('richdocuments', 'wopi_callback_url', ''))