diff --git a/README-roundcube.md b/README-roundcube.md index 25cd6d3..e833d28 100644 --- a/README-roundcube.md +++ b/README-roundcube.md @@ -1,131 +1,139 @@ # Integrating into Kolab ## Configuration Set the environment variables thusly: 1. Set `AUTH` to `"ldap"` 2. Set `STORAGE` to `"chwala"` 3. Set `CHWALA_SERVER` to Chwala's file API endpoint for `GET`ing and `PUT`ing documents. 4. Change `LDAP_` entries to match the Kolab Configuration ## Server API ### Auth `POST /auth/local` with the payload ```json { "email":"some@user.com", "password":"their_password" } ``` will give you this response: ```json { "token": "" } ``` Make all further requests with the header `Authorization: Bearer `. ### Documents 1. `GET /api/documents/` returns an array of documents that have been loaded in Manticore and have been created/edited by the requesting user. 2. `GET /api/documents/:id` returns a document with the given ID. 3. `POST /api/documents/` is a request containing three fields: `id`, `title` which can be the filename, and `access` which is an _array_ of access control entries as shown in the example document. Manticore will fetch the actual file from Chwala using the provided `id`. Normally you should initialize the access array with an entry containing the creator of the session even if no one else is collaborating. 4. `PUT /api/documents/:id` lets you overwrite a document with a new one, while keeping the ID. This request can have an empty body, Manticore will take care of fetching the file like above. 5. `GET /api/documents/:id/access` returns the access control array for a document. 5. `PUT /api/documents/:id/access` lets you update the access policy. 5. `DELETE /api/documents/:uuid` deletes the document from Manticore and ends it's corresponding session. A document is of the form: ```json { "_id": "", "title": "Project Report", "created": "2015-10-16T14:30:43.651Z", "date": "", "creator": { "name": "Administrator", "email": "admin@admin.com", }, "editors": [{ "name": "Administrator", "email": "admin@admin.com", }], "access": [{ "identity": "admin@admin.com", "permission": "write" }, { "identity": "test@user.com", "permission": "read" }, { "identity": "another@user.com", "permission": "deny" }], "live": true } ``` An access array is of the form: ```json "access": [{ "identity": "admin@admin.com", "permission": "write" }, { "identity": "test@user.com", "permission": "read" }, { "identity": "another@user.com", "permission": "deny" }], ``` ##Client API ### Open When a logged-in roundcube uesr tries to open a file, Chwala should immediately equip the Manticore auth token for that user, and open up an `iframe` pointed to this location: `http://manticore_server:port/document/:document_id/:auth_token` which will contain a ready-to-use collaborative session. ### Control When embedded in Roundcube, Manticore does not draw it's own toolbar, but provides a cross-iframe API instead that lets you do the same things that the Manticore toolbar can. For controlling Manticore from Roundcube's js, the `postMessage` API's usage can be demonstrated with this example: ```js var domain = "http://manticore_server:port", manticore = iframeElement.contentWindow; manticore.postMessage({ id: 1234, name: "actionExport", format: "pdf" }, domain); window.addEventListener("message", function (event) { console.log(event.data); /* Looks like * { id: 1234, successful: true } */ }); ``` Since `postMessage` was not designed for remote function calls, the `id` property is useful for knowing which outgoing message (here, `"actionExport"`) the incoming message is a response to. Roundcube can trivially add a callback abstraction on top of this. +#### Methods + The following methods are available, prefixed with `get`, `set`, and `action`: | Method | Response | |--------|----------| |`{name: "getTitle"}`|`{value: "Current Title"}`| |`{name: "setTitle", value: "New Title"}`|`{successful: true}`| |`{name: "getExportFormats"}`|`{value: [{ format: "odt", label: "OpenDocument Text (.odt)" }]}`| |`{name: "actionExport", value: "pdf"}`|`{successful: true}`| |`{name: "actionSave"}`|`{successful: true}`| + +#### Events + +The following events are available, suffixed with `Event`. They come with no `id` field. + +1. `{name: "titleChangeEvent", value: 'New Title'}` diff --git a/client/components/titleEditor/titleEditor.controller.js b/client/components/titleEditor/titleEditor.controller.js index fca9b88..45fe214 100644 --- a/client/components/titleEditor/titleEditor.controller.js +++ b/client/components/titleEditor/titleEditor.controller.js @@ -1,67 +1,71 @@ '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.editor.broadcastIframeEvent({ + name: 'titleChanged', + value: 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 }, 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 08f20a1..ea5bbae 100644 --- a/client/components/wodo/editor.controller.js +++ b/client/components/wodo/editor.controller.js @@ -1,148 +1,158 @@ '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, - listeners = {}; + listeners = {}, + allowedOrigin; 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 broadcastIframeEvent(data) { + if (allowedOrigin) { + window.parent.postMessage(data, allowedOrigin); + } + } + 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; + 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; + $scope.editor.broadcastIframeEvent = broadcastIframeEvent; + 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; });