diff --git a/frontend/delta/js/Clipperz/PM/DataModel/DirectLogin.js b/frontend/delta/js/Clipperz/PM/DataModel/DirectLogin.js index b8f7bc5..482c13d 100644 --- a/frontend/delta/js/Clipperz/PM/DataModel/DirectLogin.js +++ b/frontend/delta/js/Clipperz/PM/DataModel/DirectLogin.js @@ -233,6 +233,8 @@ Clipperz.Base.extend(Clipperz.PM.DataModel.DirectLogin, Object, { 'serializedData': function () { return Clipperz.Async.collectResults("DirectLogin.serializedData", { + 'favicon': MochiKit.Base.method(this,'favicon'), + 'label': MochiKit.Base.method(this,'label'), 'bookmarkletVersion': MochiKit.Base.method(this, 'getValue', 'bookmarkletVersion'), 'formData': MochiKit.Base.method(this, 'getValue', 'formData'), 'formValues': MochiKit.Base.method(this, 'getValue', 'formValues'), diff --git a/frontend/delta/js/Clipperz/PM/DataModel/Record.Version.js b/frontend/delta/js/Clipperz/PM/DataModel/Record.Version.js index 09efa97..da960b9 100644 --- a/frontend/delta/js/Clipperz/PM/DataModel/Record.Version.js +++ b/frontend/delta/js/Clipperz/PM/DataModel/Record.Version.js @@ -316,6 +316,35 @@ console.log("Record.Version.hasPendingChanges"); * / }, */ + + //========================================================================= + + // TODO: this function may mix up the order of the fields + 'exportFields': function() { + var deferredResult; + var fields; + + deferredResult = new Clipperz.Async.Deferred('Record.Version.export', {trace:false}); + deferredResult.addMethod(this,'fields'); + deferredResult.addCallback(MochiKit.Base.values); + deferredResult.addCallback(MochiKit.Base.map, function(fieldIn) { + return fieldIn.content(); + }); + deferredResult.addCallback(Clipperz.Async.collectAll); + deferredResult.addCallback(function(listIn) { + return listIn.reduce(function(result, field) { + var ref = field.reference; + result[ref] = field; + delete result[ref].reference; + return result; + }, {}); + }); + + deferredResult.callback(); + + return deferredResult; + }, + //========================================================================= __syntaxFix__: "syntax fix" }); diff --git a/frontend/delta/js/Clipperz/PM/DataModel/Record.js b/frontend/delta/js/Clipperz/PM/DataModel/Record.js index 30905d4..aa3311b 100644 --- a/frontend/delta/js/Clipperz/PM/DataModel/Record.js +++ b/frontend/delta/js/Clipperz/PM/DataModel/Record.js @@ -1169,6 +1169,65 @@ console.log("Record.hasPendingChanges RESULT", result); ], {trace:false}); }, + //========================================================================= + + 'exportDirectLogins': function() { + var result; + + var directLoginsObject = this.directLogins(); + + if (Object.keys(directLoginsObject).length == 0) { + result = {}; + } else { + var callbackObject = Object.keys(directLoginsObject).reduce(function(previous, current) { + previous[current] = MochiKit.Base.method( directLoginsObject[current], 'serializedData' ); + return previous; + }, {}); + + result = Clipperz.Async.collectResults("Record.exportDirectLogins",callbackObject,{trace:false})(); + } + + return result; + }, + + 'export': function() { + var deferredResult; + var label; + var data; + var currentVersion; + var directLogins; + var currentVersionObject; + + data = {}; + currentVersion = {}; + directLogins = {}; + deferredResult = new Clipperz.Async.Deferred('Record.export', {trace:false}); + deferredResult.addMethod(this,'getCurrentRecordVersion'); + deferredResult.addCallback(function(recordVersionIn) { currentVersionObject = recordVersionIn; }) + deferredResult.addMethod(this,'fullLabel'); + deferredResult.addMethod(this,function(labelIn) {label = labelIn}); + deferredResult.addMethod(this,'exportDirectLogins'); + deferredResult.addCallback(function(directLoginsIn) { data['directLogins'] = directLoginsIn; }); + deferredResult.addCallback(function() { return currentVersionObject.getKey(); }), + deferredResult.addMethod(this,function(keyIn) { data['currentVersionKey'] = keyIn; }); + deferredResult.addMethod(this,'notes'); + deferredResult.addMethod(this,function(notesIn) { data['notes'] = notesIn; }); + deferredResult.addMethod(this,function() { currentVersion['reference'] = this.currentVersionReference(); }); + deferredResult.addCallback(function() { return currentVersionObject.exportFields(); }), + deferredResult.addCallback(function(fieldsIn) { currentVersion['fields'] = fieldsIn; }); + deferredResult.addMethod(this,function() { + return { + 'label': label, + 'data': data, + 'currentVersion': currentVersion + }; + }); + + deferredResult.callback(); + + return deferredResult; + }, + //========================================================================= __syntaxFix__: "syntax fix" }); diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DataExport.js b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DataExport.js new file mode 100644 index 0000000..13ebe3f --- /dev/null +++ b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DataExport.js @@ -0,0 +1,51 @@ +/* + +Copyright 2008-2015 Clipperz Srl + +This file is part of Clipperz, the online password manager. +For further information about its features and functionalities please +refer to http://www.clipperz.com. + +* Clipperz 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. + +* Clipperz 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 Clipperz. If not, see http://www.gnu.org/licenses/. + +*/ + +"use strict"; +Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures'); + +Clipperz.PM.UI.Components.ExtraFeatures.DataExportClass = React.createClass({ + + propTypes: { +// featureSet: React.PropTypes.oneOf(['FULL', 'EXPIRED', 'TRIAL']).isRequired, +// 'level': React.PropTypes.oneOf(['hide', 'info', 'warning', 'error']).isRequired + }, + + //========================================================================= + + render: function () { + return React.DOM.div({className:'extraFeature devicePIN'}, [ + React.DOM.h1({}, "Export"), + React.DOM.p({'className': 'link', 'onClick': MochiKit.Base.method(this, function(){ + MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'export','json'); + })}, "JSON"), + React.DOM.p({'className': 'link', 'onClick': MochiKit.Base.method(this, function(){ + MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'export','printable'); + })}, "Printable version") + ]); + }, + + //========================================================================= +}); + +Clipperz.PM.UI.Components.ExtraFeatures.DataExport = React.createFactory(Clipperz.PM.UI.Components.ExtraFeatures.DataExportClass); diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/Panels/ExtraFeaturesPanel.js b/frontend/delta/js/Clipperz/PM/UI/Components/Panels/ExtraFeaturesPanel.js index 9e9bd7b..92609e5 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/Panels/ExtraFeaturesPanel.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/Panels/ExtraFeaturesPanel.js @@ -195,7 +195,7 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({ React.DOM.p({}, "") ]) ]), - React.DOM.li({'key':'data_3'}, [ + React.DOM.li({'key':'data_3', 'onClick':this.showExtraFeatureComponent('DataExport')}, [ React.DOM.h2({}, "Export"), React.DOM.div({}, [ React.DOM.p({}, "") diff --git a/frontend/delta/js/Clipperz/PM/UI/ExportController.js b/frontend/delta/js/Clipperz/PM/UI/ExportController.js new file mode 100644 index 0000000..11c5207 --- /dev/null +++ b/frontend/delta/js/Clipperz/PM/UI/ExportController.js @@ -0,0 +1,301 @@ +/* + +Copyright 2008-2015 Clipperz Srl + +This file is part of Clipperz, the online password manager. +For further information about its features and functionalities please +refer to http://www.clipperz.com. + +* Clipperz 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. + +* Clipperz 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 Clipperz. If not, see http://www.gnu.org/licenses/. + +*/ + +Clipperz.Base.module('Clipperz.PM.UI'); + +Clipperz.PM.UI.ExportController = function(args) { + this._type = args['type'] || Clipperz.Base.exception.raise('MandatoryParameter'); + this._recordsInfo = args['recordsInfo'] || Clipperz.Base.exception.raise('MandatoryParameter'); + this._target = Clipperz.PM.Crypto.randomKey(); + + this._style = "body {"+ + " margin: 0;"+ + " padding: 0;"+ + " font-family: monospace;"+ + "}"+ + ""+ + "p {"+ + " padding-left: 1em;"+ + "}"+ + ""+ + "h1 {"+ + " color: #ff9900;"+ + " background: black;"+ + " box-shadow: 0px 5px 6px 0 rgba(0, 0, 0, 0.15);"+ + " margin: 0;"+ + " padding:1em;"+ + "}"+ + ""+ + ".progressBar {"+ + " position: absolute;"+ + " width: 100%;"+ + " margin-top: 0px;"+ + " "+ + "}"+ + ""+ + "#completed {"+ + " background: #ff9900;"+ + " color: white;"+ + " width: 0;"+ + " overflow: hidden;"+ + " font-size: 0.8em;"+ + " box-shadow: 0px 4px 6px 0 rgba(0, 0, 0, 0.15);"+ + "}"+ + ""+ + "#printableUl {"+ + " width:100%;"+ + " height:80%;"+ + " margin: 0;"+ + " padding: 0;"+ + " list-style-type: none;"+ + "}"+ + ""+ + "#printableUl li {"+ + " border: 1px solid #1863a1;"+ + " margin: 1em;"+ + ""+ + "}"+ + ""+ + "#printableUl li .label {"+ + " background: #1863a1;"+ + " color: white;"+ + " display: block;"+ + " padding: 1em;"+ + "}"+ + ""+ + "#printableUl li dl {"+ + " padding: 1em;"+ + "}"+ + ""+ + "#printableUl li dl dt {"+ + " color: darkgray;"+ + "}"+ + ""+ + "#printableUl li dl dd {"+ + " padding: 0;"+ + " margin: 0 0 .5em 0;"+ + "}"+ + ""+ + "#printableUl li .notes {"+ + " font-style: italic;"+ + " padding: 1em 0 0 1em;"+ + " display: block;"+ + "}"+ + ""; + + return this; +} + +MochiKit.Base.update(Clipperz.PM.UI.ExportController.prototype, { + + 'toString': function() { + return "Clipperz.PM.UI.ExportController"; + }, + + //----------------------------------------------------------------------------- + + 'type': function () { + return this._type; + }, + + 'recordsInfo': function () { + return this._recordsInfo; + }, + + 'target': function () { + return this._target; + }, + + //============================================================================= + + 'setWindowTitle': function (aWindow, aTitle) { + aWindow.document.title = aTitle; + }, + + 'setWindowBody': function (aWindow, anHTML) { + aWindow.document.body.innerHTML = anHTML; + }, + + //============================================================================= + + 'initialWindowSetup': function (aWindow) { + var dom = MochiKit.DOM.DIV({'id': 'main'}, + MochiKit.DOM.H1("Clipperz Exported Data (loading...)"), + MochiKit.DOM.DIV({'class': 'progressBar'}, + MochiKit.DOM.DIV({'id': 'completed'}, + MochiKit.DOM.P({'style': 'margin:0; padding:0; text-align:center;'}, MochiKit.DOM.SPAN({'id': 'nCompleted'},"0"),"/",MochiKit.DOM.SPAN({'id': 'nTotal'},"") ) + ) + ) + ); + + aWindow.document.getElementsByTagName('head')[0].appendChild( MochiKit.DOM.STYLE(this._style) ); + + this.setWindowTitle(aWindow, "Clipperz Exported Data (loading...)"); + this.setWindowBody (aWindow, MochiKit.DOM.toHTML(dom)); + }, + + //----------------------------------------------------------------------------- + + 'updateWindowWithHTMLContent': function (aWindow, anHtml) { + this.setWindowBody(aWindow, anHtml); + }, + + 'updateWindowJSON': function (aWindow, exportedJSON) { + var dom = MochiKit.DOM.DIV({'id': 'main'}, + MochiKit.DOM.H1("Clipperz Exported Data"), + MochiKit.DOM.P("You can now save the following data and load it at any time using the Clipperz import feature."), + MochiKit.DOM.TEXTAREA({'style': 'width:100%; height:80%'}, Clipperz.Base.serializeJSON(exportedJSON)) + ); + + this.setWindowTitle(aWindow, "Clipperz Exported Data"); + this.setWindowBody(aWindow, MochiKit.DOM.toHTML(dom)); + }, + + 'updateWindowPrintable': function (aWindow, exportedJSON) { + var dom = MochiKit.DOM.DIV({'id': 'main'}, + MochiKit.DOM.H1("Clipperz Exported Data"), + MochiKit.DOM.P("You can now print this page and store it in a safe place."), + MochiKit.DOM.UL({'id': 'printableUl'}, + exportedJSON.map(function(card){ + var label = (card.label.indexOf('')>=0) ? card.label.slice(0,card.label.indexOf('')).trim() : card.label; + var notes = (card.data.notes) ? MochiKit.DOM.SPAN({'class': 'notes'}, card.data.notes) : ""; + + return MochiKit.DOM.LI({}, + MochiKit.DOM.SPAN({'class': 'label'}, label), + notes, + MochiKit.DOM.DL({}, + Object.keys(card.currentVersion.fields).map(function(key) { + return [ + MochiKit.DOM.DT(card.currentVersion.fields[key].label), + MochiKit.DOM.DD(card.currentVersion.fields[key].value), + ]; + }) + ) + ); + }) + ) + ); + + this.setWindowTitle(aWindow, "Clipperz Exported Data"); + this.setWindowBody(aWindow, MochiKit.DOM.toHTML(dom)); + }, + + 'updateWindowError': function (aWindow, errorMessage) { + this.setWindowBody(aWindow, + "

Error

"+ + "

The following error occured while exporting your data:

"+ + ""+errorMessage+"" + ); + }, + + //============================================================================= + + 'runExportJSON': function (aWindow) { + var deferredResult; + var exportedRecords; + + var totalRecords = this.recordsInfo().length; + + exportedRecords = 0; + + deferredResult = new Clipperz.Async.Deferred("DirectLoginRunner.exportJSON", {trace:false}); + deferredResult.addMethod(this, 'initialWindowSetup', aWindow); + deferredResult.addCallback(function() { return "Export Data"}); + deferredResult.addMethod(this, 'setWindowTitle', aWindow); + + deferredResult.addMethod( this, function() { return this.recordsInfo(); }); + deferredResult.addCallback( MochiKit.Base.map, function(recordIn) { + var dr = new Clipperz.Async.Deferred("DirectLoginRunner.exportJSON__exportRecord", {trace:false}); + dr.addMethod(recordIn._rowObject, 'export'); + dr.addCallback(MochiKit.Base.method(this, function (exportedRecord) { + var percentage = Math.round(100*exportedRecords/totalRecords); + + aWindow.document.getElementById('nCompleted').innerText = ++exportedRecords; + aWindow.document.getElementById('nTotal').innerText = totalRecords; + aWindow.document.getElementById('completed').style.width = percentage+'%'; + + return exportedRecord; + })); + dr.callback(); + return dr; + }); + + deferredResult.addCallback(Clipperz.Async.collectAll); + deferredResult.addMethod( this, function(exportedJSONIn) { +// console.log('return',exportedJSONIn); + + sortedJSON = exportedJSONIn.sort( function(a,b) { return a.label.toUpperCase().localeCompare(b.label.toUpperCase()); } ); + + switch (this.type()) { + case 'json': + this.updateWindowJSON(aWindow,exportedJSONIn); + break; + case 'printable': + this.updateWindowPrintable(aWindow,exportedJSONIn); + break; + default: + this.updateWindowError(aWindow,"ExportController.runExportJSON: invalid value '"+this.type()+"' for parameter 'type'."); + } + }); + + deferredResult.callback(); + + return deferredResult; + }, + + //============================================================================= + + 'run': function () { + var newWindow; + + newWindow = window.open("", this.target()); + + return this.runExportJSON(newWindow); + }, + + //============================================================================= + + 'test': function () { + var iFrame; + var newWindow; + + iFrame = MochiKit.DOM.createDOM('iframe'); + MochiKit.DOM.appendChildNodes(MochiKit.DOM.currentDocument().body, iFrame); + + newWindow = iFrame.contentWindow; + + return this.runDirectLogin(newWindow); + }, + + //============================================================================= + __syntaxFix__: "syntax fix" +}); + +//----------------------------------------------------------------------------- + +Clipperz.PM.UI.ExportController.exportJSON = function (recordsInfoIn, typeIn) { + var runner; + + runner = new Clipperz.PM.UI.ExportController({type:typeIn, recordsInfo: recordsInfoIn}); + return runner.run(); +}; diff --git a/frontend/delta/js/Clipperz/PM/UI/MainController.js b/frontend/delta/js/Clipperz/PM/UI/MainController.js index 255a149..7b8a5cb 100644 --- a/frontend/delta/js/Clipperz/PM/UI/MainController.js +++ b/frontend/delta/js/Clipperz/PM/UI/MainController.js @@ -63,6 +63,7 @@ Clipperz.PM.UI.MainController = function() { this.registerForNotificationCenterEvents([ 'doLogin', 'registerNewUser', 'showRegistrationForm', 'goBack', 'changePassphrase', 'deleteAccount', + 'export', 'toggleSelectionPanel', 'toggleSettingsPanel', 'matchMediaQuery', 'unmatchMediaQuery', 'selectAllCards', 'selectRecentCards', 'search', 'tagSelected', 'selectUntaggedCards', @@ -499,6 +500,9 @@ console.log("THE BROWSER IS OFFLINE"); deferredResult = new Clipperz.Async.Deferred('MainController.updateSelectedCard', {trace:false}); deferredResult.addMethod(this.user(), 'getRecord', someInfo['reference']); + +// deferredResult.addMethod(this, function(d) {console.log(d); return d;}); + deferredResult.addMethod(this, 'collectRecordInfo'); deferredResult.addMethod(this, 'setPageProperties', 'mainPage', 'selectedCard'); @@ -1234,6 +1238,12 @@ console.log("THE BROWSER IS OFFLINE"); this.updateSelectedCard({'reference':aRecordReference}, false, true); }, + //---------------------------------------------------------------------------- + + export_handler: function(exportType) { + return Clipperz.PM.UI.ExportController.exportJSON( this.recordsInfo(), exportType ); + }, + //---------------------------------------------------------------------------- changePassphrase_handler: function(newPassphrase) { diff --git a/frontend/delta/properties/delta.properties.json b/frontend/delta/properties/delta.properties.json index efc063d..bbfa4a4 100644 --- a/frontend/delta/properties/delta.properties.json +++ b/frontend/delta/properties/delta.properties.json @@ -170,6 +170,7 @@ "Clipperz/PM/UI/Components/ExtraFeatures/DevicePIN.js", "Clipperz/PM/UI/Components/ExtraFeatures/Passphrase.js", "Clipperz/PM/UI/Components/ExtraFeatures/DeleteAccount.js", + "Clipperz/PM/UI/Components/ExtraFeatures/DataExport.js", "Clipperz/PM/UI/Components/Cards/FavIcon.js", "Clipperz/PM/UI/Components/Cards/List.js", @@ -186,6 +187,7 @@ "Clipperz/PM/UI/MainController.js", "-- Clipperz/PM/UI/MainDesktopController.js", "Clipperz/PM/UI/DirectLoginController.js", + "Clipperz/PM/UI/ExportController.js", "main.js" ],