diff --git a/README-roundcube.md b/README-roundcube.md new file mode 100644 index 0000000..c4a256d --- /dev/null +++ b/README-roundcube.md @@ -0,0 +1,131 @@ +# 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. + + 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}`| diff --git a/README.md b/README.md index 47d1f00..9764cdf 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,36 @@ -_This is totally not ready yet. Not even pre-alpha. Only boilerplate scaffolding. Don't look!_ - # Manticore Realtime collaboration for rich office documents. ## Setup 1. Install [MongoDB](https://www.mongodb.org/) via your package manager. 2. `npm install -g bower grunt-cli` 3. Get server dependencies: `npm install` 4. Get client dependencies: `bower install` +Optionally, run a [locodoc](https://github.com/adityab/locodoc) server for the ability to export to other document formats (`pdf`, `doc`, `docx`, `txt`). + ## Run ### For frontend development `grunt serve` Runs the node server which listens at `localhost:9000`. This doesn't gracefully shut down the server due to [grunt-express-server](https://github.com/ericclemmons/grunt-express-server) eating the relevant signals. So you'll need to manually kill the node process, till there is a better solution. ### For backend development `node server/app.js` Does the same as the `grunt serve`, except that you don't get live-reloading when you change some code. Gracefully persists objects and disconnects clients and the DB when you `SIGTERM` or `SIGINT` it (just do Ctrl+C). ## Configure All environment variables are stored in `server/config/local.env.js`. Since it is unwise to checkin sensitive information into version control, this file is blacklisted in `.gitignore`. To get your own usable version, copy the existing `local.env.sample.js` under `server/config` to the aforementioned file, and then make the necessary modifications for your deployment. ## Develop Install [AngularJS Batarang](https://chrome.google.com/webstore/detail/angularjs-batarang/ighdmehidhipcmcojjgiloacoafjmpfk) from the Chrome extensions store. This will let you inspect live Angular scopes, among other things. diff --git a/client/components/exportButton/exportButton.controller.js b/client/components/exportButton/exportButton.controller.js index 4bd068d..284b9a2 100644 --- a/client/components/exportButton/exportButton.controller.js +++ b/client/components/exportButton/exportButton.controller.js @@ -1,107 +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)' }, ]); } /** * @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) { + $scope.export(event.data.value, 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/titleEditor/titleEditor.controller.js b/client/components/titleEditor/titleEditor.controller.js index dbf7682..fca9b88 100644 --- a/client/components/titleEditor/titleEditor.controller.js +++ b/client/components/titleEditor/titleEditor.controller.js @@ -1,68 +1,67 @@ '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 + 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(); });