Add local editing button to Nextcloud Office

Squashed commits:
 - Fix CSP tests
 - Update logic on opening file locally to prevent race condition with collabora post message API
 - Unlock file before opening locally
 - Add confirmation dialog
 - Toolbar button: open file locally

Signed-off-by: raul <raul@nextcloud.com>
Signed-off-by: Raul <r.ferreira.fuentes@gmail.com>
This commit is contained in:
Raul 2022-09-08 14:12:20 +02:00
parent 277c4e3895
commit e927907c48
5 changed files with 94 additions and 9 deletions

4
img/launch.svg Normal file
View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z" />
</svg>

After

Width:  |  Height:  |  Size: 270 B

View file

@ -68,6 +68,7 @@ class CSPListener implements IEventListener {
$policy = new EmptyContentSecurityPolicy();
$policy->addAllowedFrameDomain("'self'");
$policy->addAllowedFrameDomain("nc:");
foreach ($urls as $url) {
$policy->addAllowedFrameDomain($url);

View file

@ -1,5 +1,5 @@
import { emit } from '@nextcloud/event-bus'
import { getRootUrl } from '@nextcloud/router'
import { getRootUrl, imagePath } from '@nextcloud/router'
import { getRequestToken } from '@nextcloud/auth'
import Config from './services/config.tsx'
import { setGuestName, shouldAskForGuestName } from './helpers/guestName'
@ -11,9 +11,10 @@ import {
isDirectEditing,
isMobileInterfaceAvailable,
} from './helpers/mobile'
import { getWopiUrl, getSearchParam } from './helpers/url'
import { getWopiUrl, getSearchParam, getNextcloudUrl } from './helpers/url'
import '../css/document.scss'
import axios from '@nextcloud/axios'
const PostMessages = new PostMessageService({
parent: window.parent,
@ -307,6 +308,15 @@ const documentsMain = {
if (Config.get('userId') === null) {
PostMessages.sendWOPIPostMessage('loolframe', 'Hide_Menu_Item', { id: 'insertgraphicremote' })
} else {
PostMessages.sendWOPIPostMessage('loolframe', 'Insert_Button', {
id: 'Open_Local_Editor',
imgurl: window.location.protocol + '//' + getNextcloudUrl() + imagePath('richdocuments', 'launch.svg'),
mobile: false,
label: t('richdocuments', 'Open in local editor'),
hint: t('richdocuments', 'Open in local editor'),
insertBefore: 'undo',
})
}
emit('richdocuments:wopi-load:succeeded', {
@ -442,6 +452,23 @@ const documentsMain = {
case 'File_Rename':
documentsMain.fileName = args.NewName
break
case 'Clicked_Button':
if (parsed.args.Id === 'Open_Local_Editor') {
documentsMain.UI.initiateOpenLocally()
} else {
console.debug('[document] Unhandled `Clicked_Button` post message', parsed)
}
break
case 'Get_Views_Resp':
if (documentsMain.openingLocally) {
documentsMain.UI.removeViews(parsed.args)
documentsMain.unlockFile()
.catch(_ => {}) // Unlocking failed, possibly because file was not locked, we want to proceed regardless.
.then(() => {
documentsMain.openLocally()
})
}
break
default:
console.debug('[document] Unhandled post message', parsed)
}
@ -500,6 +527,53 @@ const documentsMain = {
$(document.body).removeClass('claro')
})
},
initiateOpenLocally() {
OC.dialogs.confirmDestructive(
t('richdocuments', 'When opening a file locally, the document will close for all users currently viewing the document.'),
t('richdocuments', 'Open file locally'),
{
type: OC.dialogs.YES_NO_BUTTONS,
confirm: t('richdocuments', 'Open locally'),
confirmClasses: 'error',
cancel: t('richdocuments', 'Continue editing online'),
},
(decision) => {
if (!decision) {
return
}
documentsMain.openingLocally = true
PostMessages.sendWOPIPostMessage('loolframe', 'Get_Views')
}
)
},
removeViews(views) {
PostMessages.sendWOPIPostMessage('loolframe', 'Action_Save', {
DontTerminateEdit: false,
DontSaveIfUnmodified: false,
Notify: false,
})
views.forEach((view) => {
PostMessages.sendWOPIPostMessage('loolframe', 'Action_RemoveView', { ViewId: Number(view.ViewId) })
})
},
},
unlockFile() {
const unlockUrl = getRootUrl() + '/index.php/apps/richdocuments/wopi/files/' + documentsMain.fileId
const unlockConfig = {
headers: { 'X-WOPI-Override': 'UNLOCK' }
}
return axios.post(unlockUrl, { access_token: documentsMain.token }, unlockConfig)
},
openLocally() {
if (documentsMain.openingLocally) {
documentsMain.openingLocally = false
window.location.href = 'nc://open/' + Config.get('userId') + '@' + getNextcloudUrl() + OC.encodePath(documentsMain.fullPath)
}
},
onStartup() {
@ -560,6 +634,7 @@ const documentsMain = {
$('footer,nav').show()
documentsMain.UI.hideEditor()
documentsMain.openLocally()
PostMessages.sendPostMessage('parent', 'close', '*')
},

View file

@ -86,6 +86,10 @@ const getDocumentUrlForFile = (fileDir, fileId) => {
})
}
const getNextcloudUrl = () => {
return window.location.host + (window.location.port ? `:${window.location.port}` : '')
}
export {
getSearchParam,
getWopiUrl,
@ -93,4 +97,5 @@ export {
getDocumentUrlFromTemplate,
getDocumentUrlForPublicFile,
getDocumentUrlForFile,
getNextcloudUrl,
}

View file

@ -96,7 +96,7 @@ class CSPListenerTest extends TestCase {
$policy = $this->getMergedPolicy();
self::assertEquals(["'self'", "http://public"], $policy->getAllowedFrameDomains());
self::assertEquals(["'self'", "nc:" , "http://public"], $policy->getAllowedFrameDomains());
self::assertEquals(["'self'", "http://public"], $policy->getAllowedFormActionDomains());
}
@ -123,7 +123,7 @@ class CSPListenerTest extends TestCase {
$policy = $this->getMergedPolicy();
self::assertEquals(["'self'"], $policy->getAllowedFrameDomains());
self::assertEquals(["'self'", "nc:"], $policy->getAllowedFrameDomains());
self::assertEquals(["'self'"], $policy->getAllowedFormActionDomains());
}
@ -135,7 +135,7 @@ class CSPListenerTest extends TestCase {
$policy = $this->getMergedPolicy();
self::assertEquals(["'self'", "http://public"], $policy->getAllowedFrameDomains());
self::assertEquals(["'self'", "nc:", "http://public"], $policy->getAllowedFrameDomains());
self::assertEquals(["'self'", "http://public"], $policy->getAllowedFormActionDomains());
}
@ -147,7 +147,7 @@ class CSPListenerTest extends TestCase {
$policy = $this->getMergedPolicy();
self::assertEquals(["'self'", "https://public"], $policy->getAllowedFrameDomains());
self::assertEquals(["'self'", "nc:", "https://public"], $policy->getAllowedFrameDomains());
self::assertEquals(["'self'", "https://public"], $policy->getAllowedFormActionDomains());
}
@ -165,7 +165,7 @@ class CSPListenerTest extends TestCase {
$policy = $this->getMergedPolicy();
self::assertEquals(["'self'", "https://public", "*.example.com"], $policy->getAllowedFrameDomains());
self::assertEquals(["'self'", "nc:", "https://public", "*.example.com"], $policy->getAllowedFrameDomains());
self::assertEquals(["'self'", "https://public", "*.example.com"], $policy->getAllowedFormActionDomains());
}
@ -180,7 +180,7 @@ class CSPListenerTest extends TestCase {
$policy = $this->getMergedPolicy();
self::assertEquals(["'self'", "http://internal"], $policy->getAllowedFrameDomains());
self::assertEquals(["'self'", "nc:", "http://internal"], $policy->getAllowedFrameDomains());
self::assertEquals(["'self'", "http://internal"], $policy->getAllowedFormActionDomains());
}
@ -205,7 +205,7 @@ class CSPListenerTest extends TestCase {
$policy = $manager->getDefaultPolicy();
self::assertArrayUnordered(["'self'", "external.example.com", "http://public"], $policy->getAllowedFrameDomains(), "Domains are equal", 0.0, 10, true);
self::assertArrayUnordered(["'self'", "external.example.com", "http://public", "nc:"], $policy->getAllowedFrameDomains(), "Domains are equal", 0.0, 10, true);
self::assertArrayUnordered(["'self'", "external.example.com", "http://public"], $policy->getAllowedFormActionDomains());
}