diff --git a/client/app/editor/editor.jade b/client/app/editor/editor.jade index 7392f51..3b2619d 100644 --- a/client/app/editor/editor.jade +++ b/client/app/editor/editor.jade @@ -1,9 +1,9 @@ -div.toolbar +div.toolbar(ng-attr-embedded='{{$root.config.chwalaHost !== undefined}}') div.toolbar-item.title title-editor div.toolbar-item export-button div.toolbar-item save-button -wodo-editor.wodo +wodo-editor.wodo(ng-attr-embedded='{{$root.config.chwalaHost !== undefined}}') diff --git a/client/app/editor/editor.styl b/client/app/editor/editor.styl index f85ee8e..685ec3b 100644 --- a/client/app/editor/editor.styl +++ b/client/app/editor/editor.styl @@ -1,17 +1,25 @@ .toolbar border none + border-bottom 1px solid lightgray background-color white height 50px +.toolbar[embedded=true] + display none .toolbar-item display inline-block height 50px float left -#wodoContainer - width 100% - height calc(100% - 50px) - top 50px - padding 0 - position absolute - background-color white +.wodo + #wodoContainer + width 100% + height calc(100% - 50px) + top 50px + padding 0 + position absolute + background-color white + &[embedded=true] + #wodoContainer + height: 100% + top: 0 diff --git a/client/components/exportButton/exportButton.controller.js b/client/components/exportButton/exportButton.controller.js index c55a0f5..4bd068d 100644 --- a/client/components/exportButton/exportButton.controller.js +++ b/client/components/exportButton/exportButton.controller.js @@ -1,73 +1,107 @@ 'use strict'; angular.module('manticoreApp') .controller('ExportButtonCtrl', function ($scope, $http, $timeout, SaveAs) { $scope.label = 'Download'; $scope.isExporting = false; $scope.conversionHost = $scope.$root.config.conversionHost; $scope.items = [ { format: 'odt', label: 'OpenDocument Text (.odt)' }, ]; if ($scope.conversionHost) { $scope.items = $scope.items.concat([ { format: 'pdf', label: 'Portable Document Format (.pdf)' }, { format: 'txt', label: 'Plain Text (.txt)' }, { format: 'docx', label: 'Microsoft Word (.docx)' }, { format: 'doc', label: 'Microsoft Word, old (.doc)' }, { format: 'html', label: 'HTML (.html)' }, ]); } - $scope.export = function (format) { + /** + * @param {!string} format + * @param {Function=} cb Optional callback that provides success value (true/false) + */ + $scope.export = function (format, cb) { $scope.label = 'Downloading...'; $scope.isExporting = true; var title = $scope.editor.getMetadata('dc:title') || $scope.document.title, fileName = title.replace(/\.[^.$]+$/, ''); $scope.editor.getDocumentAsByteArray(function (err, data) { if (format === 'odt') { SaveAs.download( [data.buffer], fileName + '.odt', { type: 'application/vnd.oasis.opendocument.text' } ); $scope.label = 'Download'; $scope.isExporting = false; + if (cb) { cb(true); } } else { var formData = new FormData(); formData.append('document', new Blob([data.buffer], { type: 'application/vnd.oasis.opendocument.text' })); $http({ method: 'POST', url: $scope.conversionHost + '/convert/' + format, data: formData, responseType: 'arraybuffer', transformRequest: angular.identity, transformResponse: angular.identity, headers: { 'Content-Type': undefined } }) .success(function (data, status, headers) { SaveAs.download( [data], fileName + '.' + format, { type: headers('content-type') } ); $timeout(function () { $scope.label = 'Download'; $scope.isExporting = false; + if (cb) { cb(true); } }); }) .error(function () { $timeout(function () { $scope.label = 'Error while downloading'; + if (cb) { cb(false); } }); $timeout(function () { $scope.label = 'Download'; $scope.isExporting = false; }, 1000); }); } }); }; + + function iframeGetExportFormats(event) { + event.source.postMessage({ + id: event.data.id, + value: angular.copy($scope.items) + }, event.origin); + } + + function iframeActionExport(event) { + $scope.export(event.data.format, function (successful) { + event.source.postMessage({ + id: event.data.id, + successful: successful + }, event.origin); + }); + } + + $scope.$watch('joined', function (online) { + if (online === undefined) { return; } + if (online) { + $scope.editor.addIframeEventListener('getExportFormats', iframeGetExportFormats); + $scope.editor.addIframeEventListener('actionExport', iframeActionExport); + } else { + $scope.editor.removeIframeEventListener('getExportFormats', iframeGetExportFormats); + $scope.editor.removeIframeEventListener('actionExport', iframeActionExport); + } + }); }); diff --git a/client/components/saveButton/saveButton.controller.js b/client/components/saveButton/saveButton.controller.js index 8ffcbfd..55defce 100644 --- a/client/components/saveButton/saveButton.controller.js +++ b/client/components/saveButton/saveButton.controller.js @@ -1,28 +1,51 @@ 'use strict'; angular.module('manticoreApp') .controller('SaveButtonCtrl', function ($scope, $timeout) { $scope.label = 'Save'; $scope.isSaving = false; - $scope.save = function () { + /** + * @param {Function=} cb Optional callback that provides success value (true/false) + */ + $scope.save = function (cb) { $scope.label = 'Saving'; $scope.isSaving = true; var socket = $scope.editor.clientAdaptor.getSocket(); socket.emit('save', function (err) { $timeout(function () { if (err) { $scope.label = 'Error while saving'; + if (cb) { cb(false); } } else { $scope.label = 'Saved just now'; + if (cb) { cb(true); } } }); $timeout(function () { $scope.label = 'Save'; $scope.isSaving = false; }, 1000); }); }; + + function iframeActionSave(event) { + $scope.save(function (successful) { + event.source.postMessage({ + id: event.data.id, + successful: successful + }, event.origin); + }); + } + + $scope.$watch('joined', function (online) { + if (online === undefined) { return; } + if (online) { + $scope.editor.addIframeEventListener('actionSave', iframeActionSave); + } else { + $scope.editor.removeIframeEventListener('actionSave', iframeActionSave); + } + }); }); diff --git a/client/components/titleEditor/titleEditor.controller.js b/client/components/titleEditor/titleEditor.controller.js index 8613d70..dbf7682 100644 --- a/client/components/titleEditor/titleEditor.controller.js +++ b/client/components/titleEditor/titleEditor.controller.js @@ -1,44 +1,68 @@ 'use strict'; /*global Wodo*/ angular.module('manticoreApp') .controller('TitleEditorCtrl', function ($scope, $timeout) { function handleTitleChanged(changes) { var title = changes.setProperties['dc:title']; if (title !== undefined && title !== $scope.title) { $timeout(function () { $scope.title = title; }); } } $scope.changeTitle = function () { if ($scope.title !== $scope.editor.getMetadata('dc:title')) { $scope.editor.setMetadata({ 'dc:title': $scope.title }); } }; $scope.handleEnterKey = function ($event) { if ($event.keyCode === 13) { $event.target.blur(); } }; + function iframeGetTitle(event) { + event.source.postMessage({ + id: event.data.id, + value: $scope.title + }, event.origin); + } + + function iframeSetTitle(event) { + $scope.title = event.data.value; + $scope.changeTitle(); + $timeout(function () { + event.source.postMessage({ + id: event.data.id, + successful: true, + error: null + }, event.origin); + }); + } + $scope.$watch('joined', function (online) { if (online === undefined) { return; } if (online) { $scope.editor.addEventListener(Wodo.EVENT_METADATACHANGED, handleTitleChanged); + $scope.editor.addIframeEventListener('getTitle', iframeGetTitle); + $scope.editor.addIframeEventListener('setTitle', iframeSetTitle); } else { $scope.editor.removeEventListener(Wodo.EVENT_METADATACHANGED, handleTitleChanged); + $scope.editor.removeIframeEventListener('getTitle', iframeGetTitle); + $scope.editor.removeIframeEventListener('setTitle', iframeSetTitle); + } }); function init() { $scope.title = $scope.document.title; } init(); }); diff --git a/client/components/wodo/editor.controller.js b/client/components/wodo/editor.controller.js index 42c0d0e..08f20a1 100644 --- a/client/components/wodo/editor.controller.js +++ b/client/components/wodo/editor.controller.js @@ -1,101 +1,148 @@ 'use strict'; /*global Wodo*/ angular.module('manticoreApp') .controller('WodoCtrl', function ($scope, Auth, Adaptor) { var editorInstance, clientAdaptor, editorOptions = { collabEditingEnabled: true, unstableFeaturesEnabled: true, imageEditingEnabled: false, hyperlinkEditingEnabled: false }, - onConnectCalled = false; + onConnectCalled = false, + listeners = {}; + + function addIframeEventListener(name, callback) { + if (!listeners[name]) { + listeners[name] = []; + } + listeners[name].push(callback); + } + + function removeIframeEventListener(name, callback) { + if (!listeners[name]) { + return; + } + + var index = listeners[name].indexOf(callback); + if (index !== -1) { + listeners[name].splice(index, 1); + } + } + + function setupCrossWindowMessaging() { + var chwalaHost = $scope.$root.config.chwalaHost; + if (chwalaHost) { + var temp = document.createElement('a'); + temp.href = 'http://localhost:8000'; + var allowedOrigin = temp.protocol + '//' + temp.host; + + window.addEventListener('message', function (event) { + if (event.origin !== allowedOrigin || !event.data.name) { + return; + } + + console.log('Received message from Roundcube: ' + event.data.name); + + var subscribers = listeners[event.data.name]; + if (subscribers && subscribers.length) { + for(var i = 0; i < subscribers.length; i += 1) { + subscribers[i](event); + } + } + }); + } + } function closeEditing() { editorInstance.leaveSession(function () { $scope.$apply(function () { $scope.joined = false; }); clientAdaptor.leaveSession(function () { console.log('Closed editing, left session.'); }); }); } function handleEditingError(error) { alert('Something went wrong!\n' + error); console.log(error); closeEditing(); } function openEditor(permission) { + setupCrossWindowMessaging(); + if (permission === 'write') { editorOptions.allFeaturesEnabled = true; editorOptions.reviewModeEnabled = false; } else { editorOptions.reviewModeEnabled = true; } Wodo.createCollabTextEditor('wodoContainer', editorOptions, function (err, editor) { editorInstance = editor; $scope.editor = editor; + $scope.editor.addIframeEventListener = addIframeEventListener; + $scope.editor.removeIframeEventListener = removeIframeEventListener; editorInstance.clientAdaptor = clientAdaptor; editorInstance.addEventListener(Wodo.EVENT_UNKNOWNERROR, handleEditingError); editorInstance.joinSession(clientAdaptor, function () { $scope.$apply(function () { $scope.joined = true; }); }); }); } function boot() { clientAdaptor = new Adaptor( $scope.document._id, Auth.getToken(), function onConnect() { console.log('onConnect'); if (onConnectCalled) { console.log('Reconnecting not yet supported'); return; } onConnectCalled = true; clientAdaptor.joinSession(function (memberId, permission) { if (!memberId) { console.log('Could not join; memberId not received'); } else { console.log('Joined with memberId ' + memberId); openEditor(permission); } }); }, function onKick() { console.log('onKick'); closeEditing(); }, function onDisconnect() { console.log('onDisconnect'); } ); } function destroy (cb) { if (editorInstance) { closeEditing(); editorInstance.destroy(cb); } else { if (clientAdaptor) { clientAdaptor.leaveSession(); clientAdaptor.destroy(); cb(); } } } this.boot = boot; this.destroy = destroy; }); diff --git a/client/components/wodo/editor.styl b/client/components/wodo/editor.styl index be79528..8b032ed 100644 --- a/client/components/wodo/editor.styl +++ b/client/components/wodo/editor.styl @@ -1,36 +1,35 @@ // // Wodo/Bootstrap overrides // // Hide member list view .webodfeditor-editor width 100% !important height 100% !important border none !important .webodfeditor-members display none .dijitToolbar background-image none !important background-color #f8f8f8 !important - border-top: 1px solid lightgray; // Resolve bootstrap conflicts and hide cursors' avatar handles .webodf-caretOverlay .caret border-right none border-top none margin none height inherit !important .handle display none !important .webodf-selectionOverlay stroke initial !important .editInfoMarker width 3px !important border-radius 0 !important box-shadow none !important outline 1px solid black diff --git a/server/config/environment/index.js b/server/config/environment/index.js index 9c277a1..2e8252c 100644 --- a/server/config/environment/index.js +++ b/server/config/environment/index.js @@ -1,80 +1,84 @@ 'use strict'; var path = require('path'); var _ = require('lodash'); function requiredProcessEnv(name) { if(!process.env[name]) { throw new Error('You must set the ' + name + ' environment variable'); } return process.env[name]; } // All configurations will extend these options // ============================================ var all = { env: process.env.NODE_ENV, // Root path of server root: path.normalize(__dirname + '/../../..'), // Server port port: process.env.PORT || 9000, // Should we populate the DB with sample data? seedDB: true, // Secret for session, you will want to change this and make it an environment variable secrets: { session: 'manticore-secret' }, // List of user roles userRoles: ['guest', 'user', 'admin'], // MongoDB connection options mongo: { options: { db: { safe: true } } }, defaultAccess: process.env.DEFAULT_ACCESS, - conversionHost: process.env.LOCODOC_SERVER, + + client: { + conversionHost: process.env.LOCODOC_SERVER, + chwalaHost: (process.env.STORAGE === 'chwala' && process.env.CHWALA_SERVER) || undefined + }, auth: { type: process.env.AUTH || 'local', 'webdav': { server: process.env.WEBDAV_SERVER, path: process.env.WEBDAV_PATH, key: process.env.AUTH_ENCRYPTION_KEY }, 'ldap': { server: process.env.LDAP_SERVER, base: process.env.LDAP_BASE, filter: process.env.LDAP_FILTER, bindDn: process.env.LDAP_BIND_DN, bindPw: process.env.LDAP_BIND_PW, key: process.env.AUTH_ENCRYPTION_KEY } }, storage: { type: process.env.STORAGE || 'local', 'webdav': { server: process.env.WEBDAV_SERVER, path: process.env.WEBDAV_PATH, key: process.env.AUTH_ENCRYPTION_KEY }, 'chwala': { server: process.env.CHWALA_SERVER } } }; // Export the config object based on the NODE_ENV // ============================================== module.exports = _.merge( all, require('./' + process.env.NODE_ENV + '.js') || {}); diff --git a/server/config/local.env.sample.js b/server/config/local.env.sample.js index e78d7b3..54ffdc5 100644 --- a/server/config/local.env.sample.js +++ b/server/config/local.env.sample.js @@ -1,67 +1,67 @@ 'use strict'; // Use local.env.js for environment variables that grunt will set when the server starts locally. // Use for your api keys, secrets, etc. This file should not be tracked by git. // // You will need to set these on the server you deploy to. module.exports = { DOMAIN: 'http://localhost:9000', SESSION_SECRET: 'manticore-secret', // Control debug level for modules using visionmedia/debug DEBUG: '', /* * Default access permissions for documents. * If a user has a link to a session and tries to open it, this represents the * access type they have if no permission has been explicitly set for them. * Possible values: 'write', 'read', 'deny'. * By default, is set to 'write' for testing purposes. * If completely outsourcing access control to a third party service (like Kolab), set it to 'deny'. * If left blank, defaults to 'deny'. */ - DEFAULT_ACCESS: 'deny', + DEFAULT_ACCESS: 'allow', /* * Supported authentication strategies. * 1. 'local' for using Manticore's built-in accounts system. Allow signups. * 2. 'webdav' for authenticating against a WebDAV server. Only login, no signups. * 3. 'ldap' for authenticating against an LDAP service. Only login, no signups. */ AUTH: 'local', /* * Supported storage backends. * 1. 'local' for storing everything in Mongo/GridFS. The fastest and most reliable way. * Can be used with any AUTH strategy. * 2. 'webdav' for two-way synchronizing of documents with a WebDAV server. * Can be used if AUTH is 'ldap' or 'webdav'; those credentials are used to talk to the storage server. * 3. 'chwala' can be used for integrating with Kolab. */ STORAGE: 'local', /* * WebDAV server config, only if AUTH or STORAGE is 'webdav'. */ WEBDAV_SERVER: 'https://demo.owncloud.org', WEBDAV_PATH: '/remote.php/webdav', CHWALA_SERVER: 'http://172.17.0.12', /* * Make sure you provide an encryption key to protect users' auth credentials. * This is necessary because the storage server may not support authentication tokens. */ AUTH_ENCRYPTION_KEY: 'suchauth123muchkey456', // LDAP server config, only if AUTH is 'ldap' LDAP_SERVER: 'ldap://172.17.0.12', LDAP_BASE: 'ou=People,dc=example,dc=org', LDAP_FILTER: '(&(objectclass=person)(|(uid={{username}})(mail={{username}})))', LDAP_BIND_DN: 'uid=binderservice,ou=Special Users,dc=example,dc=org', LDAP_BIND_PW: 'binderpass', // locodoc Server config LOCODOC_SERVER: 'http://localhost:3030' }; diff --git a/server/routes.js b/server/routes.js index c4c3470..c9503f6 100644 --- a/server/routes.js +++ b/server/routes.js @@ -1,35 +1,34 @@ /** * Main application routes */ 'use strict'; var errors = require('./components/errors'); var config = require('./config/environment'); module.exports = function(app) { // Insert routes below app.use('/api/templates', require('./api/template')); app.use('/api/documents', require('./api/document')); app.use('/api/users', require('./api/user')); app.use('/auth', require('./auth')); app.get('/config', function (req, res) { - res.json(200, { - conversionHost: config.conversionHost - }); + var configObject; + res.json(200, config.client); }); // All undefined asset or api routes should return a 404 app.route('/:url(api|auth|components|app|bower_components|assets)/*') .get(errors[404]); // All other routes (except dynamically-added genesis routes) // should redirect to the index.html app.route(/^\/(?!genesis).*/) .get(function(req, res) { res.sendfile(app.get('appPath') + '/index.html'); }); };