diff options
-rw-r--r-- | appinfo/app.php | 5 | ||||
-rw-r--r-- | lib/AppInfo/Application.php | 9 | ||||
-rw-r--r-- | src/files.js | 517 | ||||
-rw-r--r-- | src/view/FilesAppIntegration.js | 4 | ||||
-rw-r--r-- | src/view/Office.vue | 166 | ||||
-rw-r--r-- | src/viewer.js | 518 | ||||
-rw-r--r-- | templates/documents.php | 2 | ||||
-rw-r--r-- | webpack.common.js | 1 |
8 files changed, 710 insertions, 512 deletions
diff --git a/appinfo/app.php b/appinfo/app.php index 9785bc7d..087b31de 100644 --- a/appinfo/app.php +++ b/appinfo/app.php @@ -37,14 +37,13 @@ $eventDispatcher = \OC::$server->getEventDispatcher(); $eventDispatcher->addListener( 'OCA\Files::loadAdditionalScripts', function() { - \OCP\Util::addScript('richdocuments', 'viewer'); - \OCP\Util::addStyle('richdocuments', 'viewer'); + \OCP\Util::addScript('richdocuments', 'files'); } ); $eventDispatcher->addListener( 'OCA\Files_Sharing::loadAdditionalScripts', function() { - \OCP\Util::addScript('richdocuments', 'viewer'); + \OCP\Util::addScript('richdocuments', 'files'); } ); diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index b9ca89f5..bf8237f0 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -34,7 +34,9 @@ use OCA\Richdocuments\Preview\OOXML; use OCA\Richdocuments\Preview\OpenDocument; use OCA\Richdocuments\Preview\Pdf; use OCA\Richdocuments\Service\FederationService; +use OCA\Viewer\Event\LoadViewer; use OCP\AppFramework\App; +use OCP\EventDispatcher\IEventDispatcher; use OCP\IPreview; class Application extends App { @@ -44,6 +46,12 @@ class Application extends App { public function __construct(array $urlParams = array()) { parent::__construct(self::APPNAME, $urlParams); + /** @var IEventDispatcher $eventDispatcher */ + $eventDispatcher = $this->getContainer()->getServer()->query(IEventDispatcher::class); + $eventDispatcher->addListener(LoadViewer::class, function () { + \OCP\Util::addScript('richdocuments', 'viewer'); + }); + $this->getContainer()->registerCapability(Capabilities::class); } @@ -93,6 +101,7 @@ class Application extends App { $cspManager = $container->getServer()->getContentSecurityPolicyManager(); $policy = new ContentSecurityPolicy(); if ($publicWopiUrl !== '') { + $policy->addAllowedFrameDomain('\'self\''); $policy->addAllowedFrameDomain($publicWopiUrl); if (method_exists($policy, 'addAllowedFormActionDomain')) { $policy->addAllowedFormActionDomain($publicWopiUrl); diff --git a/src/files.js b/src/files.js new file mode 100644 index 00000000..1d8f862a --- /dev/null +++ b/src/files.js @@ -0,0 +1,517 @@ +import { getDocumentUrlFromTemplate, getDocumentUrlForPublicFile, getDocumentUrlForFile, getSearchParam } from './helpers/url' +import PostMessageService from './services/postMessage' +import Config from './services/config' +import Types from './helpers/types' +import FilesAppIntegration from './view/FilesAppIntegration' +import '../css/viewer.scss' + +const FRAME_DOCUMENT = 'FRAME_DOCUMENT' +const PostMessages = new PostMessageService({ + FRAME_DOCUMENT: () => document.getElementById('richdocumentsframe').contentWindow +}) + +const preloadCreate = getSearchParam('richdocuments_create') +const preloadOpen = getSearchParam('richdocuments_open') +const Preload = {} + +if (preloadCreate) { + Preload.create = { + type: getSearchParam('richdocuments_create'), + filename: getSearchParam('richdocuments_filename') + } +} + +if (preloadOpen) { + Preload.open = { + filename: preloadOpen, + id: getSearchParam('richdocuments_fileId'), + dir: getSearchParam('dir') + } +} + +const isDownloadHidden = document.getElementById('hideDownload') && document.getElementById('hideDownload').value === 'true' + +const isPublic = document.getElementById('isPublic') && document.getElementById('isPublic').value === '1' + +const odfViewer = { + + open: false, + receivedLoading: false, + isCollaboraConfigured: typeof OC.getCapabilities().richdocuments.collabora === 'object' && OC.getCapabilities().richdocuments.collabora.length !== 0, + supportedMimes: OC.getCapabilities().richdocuments.mimetypes.concat(OC.getCapabilities().richdocuments.mimetypesNoDefaultOpen), + excludeMimeFromDefaultOpen: OC.getCapabilities().richdocuments.mimetypesNoDefaultOpen, + hideDownloadMimes: ['image/jpeg', 'image/svg+xml', 'image/cgm', 'image/vnd.dxf', 'image/x-emf', 'image/x-wmf', 'image/x-wpg', 'image/x-freehand', 'image/bmp', 'image/png', 'image/gif', 'image/tiff', 'image/jpg', 'image/jpeg', 'text/plain', 'application/pdf'], + + register() { + const EDIT_ACTION_NAME = 'Edit with ' + OC.getCapabilities().richdocuments.productName + for (let mime of odfViewer.supportedMimes) { + OCA.Files.fileActions.register( + mime, + EDIT_ACTION_NAME, + 0, + OC.imagePath('core', 'actions/rename'), + this.onEdit, + t('richdocuments', 'Edit with {productName}', { productName: OC.getCapabilities().richdocuments.productName }) + ) + if (odfViewer.excludeMimeFromDefaultOpen.indexOf(mime) === -1 || isDownloadHidden) { + OCA.Files.fileActions.setDefault(mime, EDIT_ACTION_NAME) + } + } + }, + + onEdit: function(fileName, context) { + if (!odfViewer.isCollaboraConfigured) { + const setupUrl = OC.generateUrl('/settings/admin/richdocuments') + const installHint = OC.isUserAdmin() + ? `<a href="${setupUrl}">Collabora Online is not setup yet. <br />Click here to configure your own server or connect to a demo server.</a>` + : t('richdocuments', 'Collabora Online is not setup yet. Please contact your administrator.') + + if (OCP.Toast) { + OCP.Toast.error(installHint, { + isHTML: true, + timeout: 0 + }) + } else { + OC.Notification.showHtml(installHint) + } + return + } + if (odfViewer.open === true) { + return + } + odfViewer.open = true + let fileList = null + if (context) { + fileList = context.fileList + var fileDir = context.dir + var fileId = context.fileId || context.$file.attr('data-id') + var templateId = context.templateId + if (context.fileList) { + context.fileList.setViewerMode(true) + context.fileList.setPageTitle(fileName) + context.fileList.showMask() + } + } + odfViewer.receivedLoading = false + + let documentUrl = getDocumentUrlForFile(fileDir, fileId) + if (isPublic) { + documentUrl = getDocumentUrlForPublicFile(fileName, fileId) + } + if (typeof (templateId) !== 'undefined') { + documentUrl = getDocumentUrlFromTemplate(templateId, fileName, fileDir) + } + + /** + * We need to reload the page to set a proper CSP if the file is federated + * and the reload didn't happen for the exact same file + */ + const canAccessCSP = (url, callback) => { + let canEmbed = false + let frame = document.createElement('iframe') + frame.setAttribute('src', url) + frame.setAttribute('onload', () => { + canEmbed = true + }) + document.body.appendChild(frame) + setTimeout(() => { + if (!canEmbed) { + callback() + } + document.body.removeChild(frame) + }, 50) + + } + + const reloadForFederationCSP = (fileName) => { + const preloadId = Preload.open ? parseInt(Preload.open.id) : -1 + const fileModel = fileList.findFile(fileName) + const shareOwnerId = fileModel.shareOwnerId + if (typeof shareOwnerId !== 'undefined') { + const lastIndex = shareOwnerId.lastIndexOf('@') + // only redirect if remote file, not opened though reload and csp blocks the request + if (shareOwnerId.substr(lastIndex).indexOf('/') !== -1 && fileModel.id !== preloadId) { + canAccessCSP('https://' + shareOwnerId.substr(lastIndex) + '/status.php', () => { + window.location = OC.generateUrl('/apps/richdocuments/open?fileId=' + fileId) + }) + } + } + return false + } + + if (context) { + reloadForFederationCSP(fileName) + } + + OC.addStyle('richdocuments', 'mobile') + + var $iframe = $('<iframe id="richdocumentsframe" nonce="' + btoa(OC.requestToken) + '" scrolling="no" allowfullscreen src="' + documentUrl + '" />') + odfViewer.loadingTimeout = setTimeout(function() { + if (!odfViewer.receivedLoading) { + odfViewer.onClose() + OC.Notification.showTemporary(t('richdocuments', 'Failed to load {productName} - please try again later', { productName: OC.getCapabilities().richdocuments.productName || 'Collabora Online' })) + } + }, 15000) + $iframe.src = documentUrl + + $('body').css('overscroll-behavior-y', 'none') + var viewport = document.querySelector('meta[name=viewport]') + viewport.setAttribute('content', 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no') + if (isPublic) { + // force the preview to adjust its height + $('#preview').append($iframe).css({ height: '100%' }) + $('body').css({ height: '100%' }) + $('#content').addClass('full-height') + $('footer').addClass('hidden') + $('#imgframe').addClass('hidden') + $('.directLink').addClass('hidden') + $('.directDownload').addClass('hidden') + $('#controls').addClass('hidden') + $('#content').addClass('loading') + } else { + $('body').css('overflow', 'hidden') + $('#app-content').append($iframe) + $iframe.hide() + } + + $('#app-content #controls').addClass('hidden') + setTimeout(() => { + FilesAppIntegration.init({ + fileName, + fileId, + fileList, + sendPostMessage: (msgId, values) => { + PostMessages.sendWOPIPostMessage(FRAME_DOCUMENT, msgId, values) + } + }) + }) + }, + + onReceiveLoading() { + odfViewer.receivedLoading = true + $('#richdocumentsframe').show() + $('html, body').scrollTop(0) + $('#content').removeClass('loading') + FilesAppIntegration.initAfterReady() + }, + + onClose: function() { + odfViewer.open = false + clearTimeout(odfViewer.loadingTimeout) + odfViewer.receivedLoading = false + $('link[href*="richdocuments/css/mobile"]').remove() + $('#app-content #controls').removeClass('hidden') + $('#richdocumentsframe').remove() + $('.searchbox').show() + $('body').css('overflow', 'auto') + + if (isPublic) { + $('#content').removeClass('full-height') + $('footer').removeClass('hidden') + $('#imgframe').removeClass('hidden') + $('.directLink').removeClass('hidden') + $('.directDownload').removeClass('hidden') + } + + OC.Util.History.replaceState() + + FilesAppIntegration.close() + }, + + registerFilesMenu: function() { + + const registerFilesMenu = (OCA) => { + OCA.FilesLOMenu = { + attach: function(newFileMenu) { + var self = this + const document = Types.getFileType('document') + const spreadsheet = Types.getFileType('spreadsheet') + const presentation = Types.getFileType('presentation') + + newFileMenu.addMenuEntry({ + id: 'add-' + document.extension, + displayName: t('richdocuments', 'New Document'), + templateName: t('richdocuments', 'New Document') + '.' + document.extension, + iconClass: 'icon-filetype-document', + fileType: 'x-office-document', + actionHandler: function(filename) { + if (OC.getCapabilities().richdocuments.templates) { + self._openTemplatePicker('document', document.mime, filename) + } else { + self._createDocument(document.mime, filename) + } + } + }) + + newFileMenu.addMenuEntry({ + id: 'add-' + spreadsheet.extension, + displayName: t('richdocuments', 'New Spreadsheet'), + templateName: t('richdocuments', 'New Spreadsheet') + '.' + spreadsheet.extension, + iconClass: 'icon-filetype-spreadsheet', + fileType: 'x-office-spreadsheet', + actionHandler: function(filename) { + if (OC.getCapabilities().richdocuments.templates) { + self._openTemplatePicker('spreadsheet', spreadsheet.mime, filename) + } else { + self._createDocument(spreadsheet.mime, filename) + } + } + }) + + newFileMenu.addMenuEntry({ + id: 'add-' + presentation.extension, + displayName: t('richdocuments', 'New Presentation'), + templateName: t('richdocuments', 'New Presentation') + '.' + presentation.extension, + iconClass: 'icon-filetype-presentation', + fileType: 'x-office-presentation', + actionHandler: function(filename) { + if (OC.getCapabilities().richdocuments.templates) { + self._openTemplatePicker('presentation', presentation.mime, filename) + } else { + self._createDocument(presentation.mime, filename) + } + } + }) + }, + + _createDocument: function(mimetype, filename) { + OCA.Files.Files.isFileNameValid(filename) + filename = FileList.getUniqueName(filename) + + $.post( + OC.generateUrl('apps/richdocuments/ajax/documents/create'), + { mimetype: mimetype, filename: filename, dir: $('#dir').val() }, + function(response) { + if (response && response.status === 'success') { + FileList.add(response.data, { animate: true, scrollTo: true }) + } else { + OC.dialogs.alert(response.data.message, t('core', 'Could not create file')) + } + } + ) + }, + + _createDocumentFromTemplate: function(templateId, mimetype, filename) { + OCA.Files.Files.isFileNameValid(filename) + filename = FileList.getUniqueName(filename) + $.post( + OC.generateUrl('apps/richdocuments/ajax/documents/create'), + { mimetype: mimetype, filename: filename, dir: $('#dir').val() }, + function(response) { + if (response && response.status === 'success') { + FileList.add(response.data, { animate: false, scrollTo: false }) + odfViewer.onEdit(filename, { + fileId: -1, + dir: $('#dir').val(), + templateId: templateId, + fileList: FileList + }) + } else { + OC.dialogs.alert(response.data.message, t('core', 'Could not create file')) + } + } + ) + }, + + _openTemplatePicker: function(type, mimetype, filename) { + var self = this + $.ajax({ + url: OC.linkToOCS('apps/richdocuments/api/v1/templates', 2) + type, + dataType: 'json' + }).then(function(response) { + if (response.ocs.data.length === 1) { + const { id } = response.ocs.data[0] + self._createDocumentFromTemplate(id, mimetype, filename) + return + } + self._buildTemplatePicker(response.ocs.data) + .then(function() { + var buttonlist = [{ + text: t('core', 'Cancel'), + classes: 'cancel', + click: function() { + $(this).ocdialog('close') + } + }, { + text: t('richdocuments', 'Create'), + classes: 'primary', + click: function() { + var templateId = this.dataset.templateId + self._createDocumentFromTemplate(templateId, mimetype, filename) + $(this).ocdialog('close') + } + }] + + $('#template-picker').ocdialog({ + closeOnEscape: true, + modal: true, + buttons: buttonlist + }) + }) + }) + }, + + _buildTemplatePicker: function(data) { + var self = this + return $.get(OC.filePath('richdocuments', 'templates', 'templatePicker.html'), function(tmpl) { + var $tmpl = $(tmpl) + // init template picker + var $dlg = $tmpl.octemplate({ + dialog_name: 'template-picker', + dialog_title: t('richdocuments', 'Select template') + }) + + // create templates list + var templates = _.values(data) + templates.forEach(function(template) { + self._appendTemplateFromData($dlg[0], template) + }) + + $('body').append($dlg) + }) + }, + + _appendTemplateFromData: function(dlg, data) { + var template = dlg.querySelector('.template-model').cloneNode(true) + template.className = '' + template.querySelector('img').src = OC.generateUrl('apps/richdocuments/template/preview/' + data.id) + template.querySelector('h2').textContent = data.name + template.onclick = function() { + dlg.dataset.templateId = data.id + } + if (!dlg.dataset.templateId) { + dlg.dataset.templateId = data.id + } + + dlg.querySelector('.template-container').appendChild(template) + } + } + } + registerFilesMenu(OCA) + + OC.Plugins.register('OCA.Files.NewFileMenu', OCA.FilesLOMenu) + + // Open the template picker if there was a create parameter detected on load + if (Preload.create && Preload.create.type && Preload.create.filename) { + const fileType = Types.getFileType(Preload.create.type, Config.get('ooxml')) + OCA.FilesLOMenu._openTemplatePicker(Preload.create.type, fileType.mime, Preload.create.filename + '.' + fileType.extension) + } + + if (Preload.open) { + FileList.$fileList.one('updated', function() { + odfViewer.onEdit(Preload.open.filename, { + fileId: Preload.open.id, + dir: document.getElementById('dir').value, + fileList: FileList + }) + }) + } + } +} + +const settings = OC.getCapabilities()['richdocuments']['config'] || {} +Config.update('ooxml', settings['doc_format'] === 'ooxml') + +window.OCA.RichDocuments = { + config: { + create: Types.getFileTypes() + } +} + +$(document).ready(function() { + // register file actions and menu + if (typeof OCA !== 'undefined' + && typeof OCA.Files !== 'undefined' + && typeof OCA.Files.fileActions !== 'undefined' + ) { + // check if texteditor app is enabled and loaded... + if (typeof OCA.Files_Texteditor === 'undefined' && typeof OCA.Text === 'undefined') { + odfViewer.supportedMimes.push('text/plain') + } + + odfViewer.registerFilesMenu() + odfViewer.register() + } + + // Open documents if a public page is opened for a supported mimetype + const isSupportedMime = isPublic && odfViewer.supportedMimes.indexOf($('#mimetype').val()) !== -1 && odfViewer.excludeMimeFromDefaultOpen.indexOf($('#mimetype').val()) === -1 + const showSecureView = isPublic && isDownloadHidden && odfViewer.hideDownloadMimes.indexOf($('#mimetype').val()) !== -1 + if (isSupportedMime || showSecureView) { + odfViewer.onEdit(document.getElementById('filename').value) + } + + PostMessages.registerPostMessageHandler(({ parsed }) => { + console.debug('[viewer] Received post message', parsed) + const { msgId, args, deprecated } = parsed + if (deprecated) { return } + + switch (msgId) { + case 'loading': + odfViewer.onReceiveLoading() + break + case 'App_LoadingStatus': + if (args.Status === 'Timeout') { + odfViewer.onClose() + OC.Notification.showTemporary(t('richdocuments', 'Failed to connect to {productName}. Please try again later or contact your server administrator.', + { productName: OC.getCapabilities().richdocuments.productName } + )) + } + break + case 'UI_Share': + FilesAppIntegration.share() + break + case 'UI_CreateFile': + FilesAppIntegration.createNewFile(args.DocumentType) + break + case 'UI_InsertGraphic': + FilesAppIntegration.insertGraphic((filename, url) => { + PostMessages.sendWOPIPostMessage(FRAME_DOCUMENT, 'postAsset', { FileName: filename, Url: url }) + }) + break + case 'File_Rename': + FileList.reload() + OC.Apps.hideAppSidebar() + FilesAppIntegration.fileName = args.NewName + break + case 'close': + odfViewer.onClose() + break + case 'Get_Views_Resp': + case 'Views_List': + FilesAppIntegration.setViews(args) + break + case 'UI_FileVersions': + case 'rev-history': + FilesAppIntegration.showRevHistory() + break + case 'App_VersionRestore': + if (args.Status === 'Pre_Restore_Ack') { + FilesAppIntegration.restoreVersionExecute() + } + break + } + + // legacy view handling + if (msgId === 'View_Added') { + FilesAppIntegration.views[args.ViewId] = args + FilesAppIntegration.renderAvatars() + } else if (msgId === 'View_Removed') { + delete FilesAppIntegration.views[args.ViewId] + FilesAppIntegration.renderAvatars() + } else if (msgId === 'FollowUser_Changed') { + if (args.IsFollowEditor) { + FilesAppIntegration.followingEditor = true + } else { + FilesAppIntegration.followingEditor = false + } + if (args.IsFollowUser) { + FilesAppIntegration.following = args.FollowedViewId + } else { + FilesAppIntegration.following = null + } + FilesAppIntegration.renderAvatars() + } + + }) + window.FilesAppIntegration = FilesAppIntegration +}) diff --git a/src/view/FilesAppIntegration.js b/src/view/FilesAppIntegration.js index f6cba7d7..22cfd86d 100644 --- a/src/view/FilesAppIntegration.js +++ b/src/view/FilesAppIntegration.js @@ -106,10 +106,10 @@ export default { if (this.fileList) { return this.fileList } - if (OCA.Files.App) { + if (OCA.Files && OCA.Files.App) { return OCA.Files.App.fileList } - if (OCA.Sharing.PublicApp) { + if (OCA.Sharing && OCA.Sharing.PublicApp) { return OCA.Sharing.PublicApp.fileList } return null diff --git a/src/view/Office.vue b/src/view/Office.vue new file mode 100644 index 00000000..44397594 --- /dev/null +++ b/src/view/Office.vue @@ -0,0 +1,166 @@ +<!-- + - @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net> + - + - @author Julius Härtl <jus@bitgrid.net> + - + - @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 <http://www.gnu.org/licenses/>. + - + --> + +<template> + <transition name="fade" appear> + <div v-show="loading" id="richdocuments-wrapper"> + <div class="header"> + <!-- This is obviously not the way to go since it would require absolute positioning and therefore not be compatible with viewer actions/sidebar --> + <div class="avatars"> + <avatar v-for="view in avatarViews" :key="view.ViewId" :user="view.UserId" + :display-name="view.UserName" + :style="viewColor(view)" /> + </div> + </div> + <iframe id="collaboraframe" ref="documentFrame" :src="src" /> + </div> + </transition> +</template> + +<script> +import Avatar from '@nextcloud/vue/dist/Components/Avatar' + +import { getDocumentUrlForFile } from '../helpers/url' +import PostMessageService from '../services/postMessage' +import FilesAppIntegration from './FilesAppIntegration' + +const FRAME_DOCUMENT = 'FRAME_DOCUMENT' +const PostMessages = new PostMessageService({ + FRAME_DOCUMENT: () => document.getElementById('collaboraframe').contentWindow +}) + +export default { + name: 'Office', + components: { + Avatar + }, + props: { + filename: { + type: String, + default: null + }, + fileid: { + type: Number, + default: null + }, + hasPreview: { + type: Boolean, + required: false, + default: () => false + } + }, + data() { + return { + src: null, + loading: false, + views: [] + } + }, + computed: { + avatarViews() { + return this.views + }, + viewColor() { + return view => ({ + 'border-color': '#' + ('000000' + Number(view.Color).toString(16)).substr(-6), + 'border-width': '2px', + 'border-style': 'solid' + }) + } + }, + mounted() { + PostMessages.registerPostMessageHandler(({ parsed }) => { + console.debug('[viewer] Received post message', parsed) + const { msgId, args, deprecated } = parsed + if (deprecated) { return } + + switch (msgId) { + case 'loading': + break + case 'close': + this.$parent.close() + break + case 'Get_Views_Resp': + case 'Views_List': + this.views = args + break + case 'UI_InsertGraphic': + FilesAppIntegration.insertGraphic((filename, url) => { + PostMessages.sendWOPIPostMessage(FRAME_DOCUMENT, 'postAsset', { FileName: filename, Url: url }) + }) + break + } + }) + this.load() + }, + methods: { + async load() { + let documentUrl = getDocumentUrlForFile(this.filename, this.fileid) + '&path=' + this.filename + this.$emit('update:loaded', true) + this.src = documentUrl + this.loading = true + } + } +} +</script> +<style lang="scss"> + .header { + position: absolute; + right: 100px; + top: -50px; + + .avatars { + display: flex; + padding: 9px; + + .avatardiv { + margin-left: -15px; + box-shadow: 0 0 3px var(--color-box-shadow); + } + + } + } + + #richdocuments-wrapper { + width: 100vw; + height: calc(100vh - 50px); + top: 50px; + left: 0; + position: absolute; + z-index: 100000; + max-width: 100%; + display: flex; + flex-direction: column; + background-color: var(--color-main-background); + transition: opacity .25s; + } + iframe { + width: 100%; + flex-grow: 1; + } + .fade-enter-active, .fade-leave-active { + transition: opacity .25s; + } + .fade-enter, .fade-leave-to { + opacity: 0; + } +</style> diff --git a/src/viewer.js b/src/viewer.js index 5c9e2b4d..4c1d7662 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -1,516 +1,22 @@ -import { getDocumentUrlFromTemplate, getDocumentUrlForPublicFile, getDocumentUrlForFile, getSearchParam } from './helpers/url' -import PostMessageService from './services/postMessage' -import Config from './services/config' -import Types from './helpers/types' -import FilesAppIntegration from './view/FilesAppIntegration' -import '../css/viewer.scss' +import Office from './view/Office' -const FRAME_DOCUMENT = 'FRAME_DOCUMENT' -const PostMessages = new PostMessageService({ - FRAME_DOCUMENT: () => document.getElementById('richdocumentsframe').contentWindow -}) - -const preloadCreate = getSearchParam('richdocuments_create') -const preloadOpen = getSearchParam('richdocuments_open') -const Preload = {} - -if (preloadCreate) { - Preload.create = { - type: getSearchParam('richdocuments_create'), - filename: getSearchParam('richdocuments_filename') - } -} - -if (preloadOpen) { - Preload.open = { - filename: preloadOpen, - id: getSearchParam('richdocuments_fileId'), - dir: getSearchParam('dir') - } -} - -const isDownloadHidden = document.getElementById('hideDownload') && document.getElementById('hideDownload').value === 'true' - -const isPublic = document.getElementById('isPublic') && document.getElementById('isPublic').value === '1' - -const odfViewer = { - - open: false, - receivedLoading: false, - isCollaboraConfigured: typeof OC.getCapabilities().richdocuments.collabora === 'object' && OC.getCapabilities().richdocuments.collabora.length !== 0, - supportedMimes: OC.getCapabilities().richdocuments.mimetypes.concat(OC.getCapabilities().richdocuments.mimetypesNoDefaultOpen), - excludeMimeFromDefaultOpen: OC.getCapabilities().richdocuments.mimetypesNoDefaultOpen, - hideDownloadMimes: ['image/jpeg', 'image/svg+xml', 'image/cgm', 'image/vnd.dxf', 'image/x-emf', 'image/x-wmf', 'image/x-wpg', 'image/x-freehand', 'image/bmp', 'image/png', 'image/gif', 'image/tiff', 'image/jpg', 'image/jpeg', 'text/plain', 'application/pdf'], - - register() { - const EDIT_ACTION_NAME = 'Edit with ' + OC.getCapabilities().richdocuments.productName - for (let mime of odfViewer.supportedMimes) { - OCA.Files.fileActions.register( - mime, - EDIT_ACTION_NAME, - 0, - OC.imagePath('core', 'actions/rename'), - this.onEdit, - t('richdocuments', 'Edit with {productName}', { productName: OC.getCapabilities().richdocuments.productName }) - ) - if (odfViewer.excludeMimeFromDefaultOpen.indexOf(mime) === -1 || isDownloadHidden) { - OCA.Files.fileActions.setDefault(mime, EDIT_ACTION_NAME) - } - } - }, - - onEdit: function(fileName, context) { - if (!odfViewer.isCollaboraConfigured) { - const setupUrl = OC.generateUrl('/settings/admin/richdocuments') - const installHint = OC.isUserAdmin() - ? `<a href="${setupUrl}">Collabora Online is not setup yet. <br />Click here to configure your own server or connect to a demo server.</a>` - : t('richdocuments', 'Collabora Online is not setup yet. Please contact your administrator.') - - if (OCP.Toast) { - OCP.Toast.error(installHint, { - isHTML: true, - timeout: 0 - }) - } else { - OC.Notification.showHtml(installHint) - } - return - } - if (odfViewer.open === true) { - return - } - odfViewer.open = true - let fileList = null - if (context) { - fileList = context.fileList - var fileDir = context.dir - var fileId = context.fileId || context.$file.attr('data-id') - var templateId = context.templateId - if (context.fileList) { - context.fileList.setViewerMode(true) - context.fileList.setPageTitle(fileName) - context.fileList.showMask() - } - } - odfViewer.receivedLoading = false - - let documentUrl = getDocumentUrlForFile(fileDir, fileId) - if (isPublic) { - documentUrl = getDocumentUrlForPublicFile(fileName, fileId) - } - if (typeof (templateId) !== 'undefined') { - documentUrl = getDocumentUrlFromTemplate(templateId, fileName, fileDir) - } - - /** - * We need to reload the page to set a proper CSP if the file is federated - * and the reload didn't happen for the exact same file - */ - const canAccessCSP = (url, callback) => { - let canEmbed = false - let frame = document.createElement('iframe') - frame.setAttribute('src', url) - frame.setAttribute('onload', () => { - canEmbed = true - }) - document.body.appendChild(frame) - setTimeout(() => { - if (!canEmbed) { - callback() - } - document.body.removeChild(frame) - }, 50) - - } - - const reloadForFederationCSP = (fileName) => { - const preloadId = Preload.open ? parseInt(Preload.open.id) : -1 - const fileModel = fileList.findFile(fileName) - const shareOwnerId = fileModel.shareOwnerId - if (typeof shareOwnerId !== 'undefined') { - const lastIndex = shareOwnerId.lastIndexOf('@') - // only redirect if remote file, not opened though reload and csp blocks the request - if (shareOwnerId.substr(lastIndex).indexOf('/') !== -1 && fileModel.id !== preloadId) { - canAccessCSP('https://' + shareOwnerId.substr(lastIndex) + '/status.php', () => { - window.location = OC.generateUrl('/apps/richdocuments/open?fileId=' + fileId) - }) - } - } - return false - } - - if (context) { - reloadForFederationCSP(fileName) - } - - OC.addStyle('richdocuments', 'mobile') - - var $iframe = $('<iframe id="richdocumentsframe" nonce="' + btoa(OC.requestToken) + '" scrolling="no" allowfullscreen src="' + documentUrl + '" />') - odfViewer.loadingTimeout = setTimeout(function() { - if (!odfViewer.receivedLoading) { - odfViewer.onClose() - OC.Notification.showTemporary(t('richdocuments', 'Failed to load {productName} - please try again later', { productName: OC.getCapabilities().richdocuments.productName || 'Collabora Online' })) - } - }, 15000) - $iframe.src = documentUrl - - $('body').css('overscroll-behavior-y', 'none') - var viewport = document.querySelector('meta[name=viewport]') - viewport.setAttribute('content', 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no') - if (isPublic) { - // force the preview to adjust its height - $('#preview').append($iframe).css({ height: '100%' }) - $('body').css({ height: '100%' }) - $('#content').addClass('full-height') - $('footer').addClass('hidden') - $('#imgframe').addClass('hidden') - $('.directLink').addClass('hidden') - $('.directDownload').addClass('hidden') - $('#controls').addClass('hidden') - $('#content').addClass('loading') - } else { - $('body').css('overflow', 'hidden') - $('#app-content').append($iframe) - $iframe.hide() - } - - $('#app-content #controls').addClass('hidden') - setTimeout(() => { - FilesAppIntegration.init({ - fileName, - fileId, - fileList, - sendPostMessage: (msgId, values) => { - PostMessages.sendWOPIPostMessage(FRAME_DOCUMENT, msgId, values) - } - }) - }) - }, - - onReceiveLoading() { - odfViewer.receivedLoading = true - $('#richdocumentsframe').show() - $('html, body').scrollTop(0) - $('#content').removeClass('loading') - FilesAppIntegration.initAfterReady() - }, - - onClose: function() { - odfViewer.open = false - clearTimeout(odfViewer.loadingTimeout) - odfViewer.receivedLoading = false - $('link[href*="richdocuments/css/mobile"]').remove() - $('#app-content #controls').removeClass('hidden') - $('#richdocumentsframe').remove() - $('.searchbox').show() - $('body').css('overflow', 'auto') - - if (isPublic) { - $('#content').removeClass('full-height') - $('footer').removeClass('hidden') - $('#imgframe').removeClass('hidden') - $('.directLink').removeClass('hidden') - $('.directDownload').removeClass('hidden') - } - - OC.Util.History.replaceState() - - FilesAppIntegration.close() - }, +const supportedMimes = OC.getCapabilities().richdocuments.mimetypes.concat(OC.getCapabilities().richdocuments.mimetypesNoDefaultOpen) - registerFilesMenu: function() { - - const registerFilesMenu = (OCA) => { - OCA.FilesLOMenu = { - attach: function(newFileMenu) { - var self = this - const document = Types.getFileType('document') - const spreadsheet = Types.getFileType('spreadsheet') - const presentation = Types.getFileType('presentation') - - newFileMenu.addMenuEntry({ - id: 'add-' + document.extension, - displayName: t('richdocuments', 'New Document'), - templateName: t('richdocuments', 'New Document') + '.' + document.extension, - iconClass: 'icon-filetype-document', - fileType: 'x-office-document', - actionHandler: function(filename) { - if (OC.getCapabilities().richdocuments.templates) { - self._openTemplatePicker('document', document.mime, filename) - } else { - self._createDocument(document.mime, filename) - } - } - }) - - newFileMenu.addMenuEntry({ - id: 'add-' + spreadsheet.extension, - displayName: t('richdocuments', 'New Spreadsheet'), - templateName: t('richdocuments', 'New Spreadsheet') + '.' + spreadsheet.extension, - iconClass: 'icon-filetype-spreadsheet', - fileType: 'x-office-spreadsheet', - actionHandler: function(filename) { - if (OC.getCapabilities().richdocuments.templates) { - self._openTemplatePicker('spreadsheet', spreadsheet.mime, filename) - } else { - self._createDocument(spreadsheet.mime, filename) - } - } - }) - - newFileMenu.addMenuEntry({ - id: 'add-' + presentation.extension, - displayName: t('richdocuments', 'New Presentation'), - templateName: t('richdocuments', 'New Presentation') + '.' + presentation.extension, - iconClass: 'icon-filetype-presentation', - fileType: 'x-office-presentation', - actionHandler: function(filename) { - if (OC.getCapabilities().richdocuments.templates) { - self._openTemplatePicker('presentation', presentation.mime, filename) - } else { - self._createDocument(presentation.mime, filename) - } - } - }) - }, - - _createDocument: function(mimetype, filename) { - OCA.Files.Files.isFileNameValid(filename) - filename = FileList.getUniqueName(filename) - - $.post( - OC.generateUrl('apps/richdocuments/ajax/documents/create'), - { mimetype: mimetype, filename: filename, dir: $('#dir').val() }, - function(response) { - if (response && response.status === 'success') { - FileList.add(response.data, { animate: true, scrollTo: true }) - } else { - OC.dialogs.alert(response.data.message, t('core', 'Could not create file')) - } - } - ) - }, - - _createDocumentFromTemplate: function(templateId, mimetype, filename) { - OCA.Files.Files.isFileNameValid(filename) - filename = FileList.getUniqueName(filename) - $.post( - OC.generateUrl('apps/richdocuments/ajax/documents/create'), - { mimetype: mimetype, filename: filename, dir: $('#dir').val() }, - function(response) { - if (response && response.status === 'success') { - FileList.add(response.data, { animate: false, scrollTo: false }) - odfViewer.onEdit(filename, { - fileId: -1, - dir: $('#dir').val(), - templateId: templateId, - fileList: FileList - }) - } else { - OC.dialogs.alert(response.data.message, t('core', 'Could not create file')) - } - } - ) - }, - - _openTemplatePicker: function(type, mimetype, filename) { - var self = this - $.ajax({ - url: OC.linkToOCS('apps/richdocuments/api/v1/templates', 2) + type, - dataType: 'json' - }).then(function(response) { - if (response.ocs.data.length === 1) { - const { id } = response.ocs.data[0] - self._createDocumentFromTemplate(id, mimetype, filename) - return - } - self._buildTemplatePicker(response.ocs.data) - .then(function() { - var buttonlist = [{ - text: t('core', 'Cancel'), - classes: 'cancel', - click: function() { - $(this).ocdialog('close') - } - }, { - text: t('richdocuments', 'Create'), - classes: 'primary', - click: function() { - var templateId = this.dataset.templateId - self._createDocumentFromTemplate(templateId, mimetype, filename) - $(this).ocdialog('close') - } - }] - - $('#template-picker').ocdialog({ - closeOnEscape: true, - modal: true, - buttons: buttonlist - }) - }) - }) - }, - - _buildTemplatePicker: function(data) { - var self = this - return $.get(OC.filePath('richdocuments', 'templates', 'templatePicker.html'), function(tmpl) { - var $tmpl = $(tmpl) - // init template picker - var $dlg = $tmpl.octemplate({ - dialog_name: 'template-picker', - dialog_title: t('richdocuments', 'Select template') - }) - - // create templates list - var templates = _.values(data) - templates.forEach(function(template) { - self._appendTemplateFromData($dlg[0], template) - }) - - $('body').append($dlg) - }) - }, - - _appendTemplateFromData: function(dlg, data) { - var template = dlg.querySelector('.template-model').cloneNode(true) - template.className = '' - template.querySelector('img').src = OC.generateUrl('apps/richdocuments/template/preview/' + data.id) - template.querySelector('h2').textContent = data.name - template.onclick = function() { - dlg.dataset.templateId = data.id - } - if (!dlg.dataset.templateId) { - dlg.dataset.templateId = data.id - } - - dlg.querySelector('.template-container').appendChild(template) - } - } - } - registerFilesMenu(OCA) - - OC.Plugins.register('OCA.Files.NewFileMenu', OCA.FilesLOMenu) - - // Open the template picker if there was a create parameter detected on load - if (Preload.create && Preload.create.type && Preload.create.filename) { - const fileType = Types.getFileType(Preload.create.type, Config.get('ooxml')) - OCA.FilesLOMenu._openTemplatePicker(Preload.create.type, fileType.mime, Preload.create.filename + '.' + fileType.extension) - } - - if (Preload.open) { - FileList.$fileList.one('updated', function() { - odfViewer.onEdit(Preload.open.filename, { - fileId: Preload.open.id, - dir: document.getElementById('dir').value, - fileList: FileList - }) - }) - } - } -} - -const settings = OC.getCapabilities()['richdocuments']['config'] || {} -Config.update('ooxml', settings['doc_format'] === 'ooxml') - -window.OCA.RichDocuments = { - config: { - create: Types.getFileTypes() - } -} - -$(document).ready(function() { - // register file actions and menu +document.addEventListener('DOMContentLoaded', function(event) { + // Only use it outside the files app for now if (typeof OCA !== 'undefined' && typeof OCA.Files !== 'undefined' && typeof OCA.Files.fileActions !== 'undefined' ) { - // check if texteditor app is enabled and loaded... - if (typeof OCA.Files_Texteditor === 'undefined' && typeof OCA.Text === 'undefined') { - odfViewer.supportedMimes.push('text/plain') - } - odfViewer.register() - odfViewer.registerFilesMenu() + return } - // Open documents if a public page is opened for a supported mimetype - const isSupportedMime = isPublic && odfViewer.supportedMimes.indexOf($('#mimetype').val()) !== -1 && odfViewer.excludeMimeFromDefaultOpen.indexOf($('#mimetype').val()) === -1 - const showSecureView = isPublic && isDownloadHidden && odfViewer.hideDownloadMimes.indexOf($('#mimetype').val()) !== -1 - if (isSupportedMime || showSecureView) { - odfViewer.onEdit(document.getElementById('filename').value) + if (OCA.Viewer) { + OCA.Viewer.registerHandler({ + id: 'richdocuments', + group: null, + mimes: supportedMimes, + component: Office + }) } - - PostMessages.registerPostMessageHandler(({ parsed }) => { - console.debug('[viewer] Received post message', parsed) - const { msgId, args, deprecated } = parsed - if (deprecated) { return } - - switch (msgId) { - case 'loading': - odfViewer.onReceiveLoading() - break - case 'App_LoadingStatus': - if (args.Status === 'Timeout') { - odfViewer.onClose() - OC.Notification.showTemporary(t('richdocuments', 'Failed to connect to {productName}. Please try again later or contact your server administrator.', - { productName: OC.getCapabilities().richdocuments.productName } - )) - } - break - case 'UI_Share': - FilesAppIntegration.share() - break - case 'UI_CreateFile': - FilesAppIntegration.createNewFile(args.DocumentType) - break - case 'UI_InsertGraphic': - FilesAppIntegration.insertGraphic((filename, url) => { - PostMessages.sendWOPIPostMessage(FRAME_DOCUMENT, 'postAsset', { FileName: filename, Url: url }) - }) - break - case 'File_Rename': - FileList.reload() - OC.Apps.hideAppSidebar() - FilesAppIntegration.fileName = args.NewName - break - case 'close': - odfViewer.onClose() - break - case 'Get_Views_Resp': - case 'Views_List': - FilesAppIntegration.setViews(args) - break - case 'UI_FileVersions': - case 'rev-history': - FilesAppIntegration.showRevHistory() - break - case 'App_VersionRestore': - if (args.Status === 'Pre_Restore_Ack') { - FilesAppIntegration.restoreVersionExecute() - } - break - } - - // legacy view handling - if (msgId === 'View_Added') { - FilesAppIntegration.views[args.ViewId] = args - FilesAppIntegration.renderAvatars() - } else if (msgId === 'View_Removed') { - delete FilesAppIntegration.views[args.ViewId] - FilesAppIntegration.renderAvatars() - } else if (msgId === 'FollowUser_Changed') { - if (args.IsFollowEditor) { - FilesAppIntegration.followingEditor = true - } else { - FilesAppIntegration.followingEditor = false - } - if (args.IsFollowUser) { - FilesAppIntegration.following = args.FollowedViewId - } else { - FilesAppIntegration.following = null - } - FilesAppIntegration.renderAvatars() - } - - }) - window.FilesAppIntegration = FilesAppIntegration }) diff --git a/templates/documents.php b/templates/documents.php index 50d7b1a3..877cc7a9 100644 --- a/templates/documents.php +++ b/templates/documents.php @@ -14,5 +14,5 @@ <?php script('richdocuments', 'document'); ?> -<div id="loadingContainer" class="icon-loading"></div> +<div id="loadingContainer"></div> <div id="documents-content"></div> diff --git a/webpack.common.js b/webpack.common.js index d905fe36..db3a21a8 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -6,6 +6,7 @@ const StyleLintPlugin = require('stylelint-webpack-plugin'); module.exports = { entry: { viewer: path.join(__dirname, 'src', 'viewer.js'), + files: path.join(__dirname, 'src', 'files.js'), document: path.join(__dirname, 'src', 'document.js'), admin: path.join(__dirname, 'src', 'admin.js'), personal: path.join(__dirname, 'src', 'personal.js'), |