1
0
mirror of http://git.whoc.org.uk/git/password-manager.git synced 2025-10-29 18:37:35 +01:00

Reviewed and improved export feature

This commit is contained in:
Giulio Cesare Solaroli
2015-05-07 16:23:57 +02:00
parent b812ea4efb
commit e329f6926a
18 changed files with 1195 additions and 474 deletions

View File

@@ -332,12 +332,13 @@ console.log("Record.Version.hasPendingChanges");
});
deferredResult.addCallback(Clipperz.Async.collectAll);
deferredResult.addCallback(function(listIn) {
return listIn.reduce(function(result, field) {
// return listIn.reduce(function(result, field) {
return MochiKit.Iter.reduce(function(result, field) {
var ref = field.reference;
result[ref] = field;
delete result[ref].reference;
return result;
}, {});
}, listIn, {});
});
deferredResult.callback();

View File

@@ -45,9 +45,7 @@ Clipperz.PM.DataModel.Record = function(args) {
this._createNewDirectLoginFunction = args.createNewDirectLoginFunction || null;
this._tags = [];
this._directLogins = {};
this._versions = {};
this._currentRecordVersion = null;
@@ -163,34 +161,20 @@ Clipperz.Base.extend(Clipperz.PM.DataModel.Record, Clipperz.PM.DataModel.Encrypt
//............................................................................
'tagRegExp': function () {
return new RegExp('\\' + Clipperz.PM.DataModel.Record.tagChar + '(' + Clipperz.PM.DataModel.Record.specialTagChar + '?\\w+)', 'g');
'extractLabelFromFullLabel': function (aValue) {
return Clipperz.PM.DataModel.Record.extractLabelFromFullLabel(aValue);
},
'trimSpacesRegExp': function () {
return new RegExp('^\\s+|\\s+$', 'g');
'extractTagsFromFullLabel': function (aLabel) {
return Clipperz.PM.DataModel.Record.extractTagsFromFullLabel(aLabel);
},
// 'tagCleanupRegExp': function () {
// return new RegExp('\\' + Clipperz.PM.DataModel.Record.tagSpace, 'g');
// },
//............................................................................
'filterOutTags': function (aValue) {
var value;
value = aValue;
value = value.replace(this.tagRegExp(), '');
value = value.replace(this.trimSpacesRegExp(), '');
return value;
},
'label': function () {
return Clipperz.Async.callbacks("Record.label", [
MochiKit.Base.method(this, 'fullLabel'),
MochiKit.Base.method(this, 'filterOutTags')
MochiKit.Base.method(this, 'extractLabelFromFullLabel')
], {trace:false});
},
@@ -211,22 +195,6 @@ Clipperz.Base.extend(Clipperz.PM.DataModel.Record, Clipperz.PM.DataModel.Encrypt
//.........................................................................
'extractTagsFromFullLabel': function (aLabel) {
var tagRegEx;
var result;
var match;
result = {};
tagRegEx = this.tagRegExp();
match = tagRegEx.exec(aLabel);
while (match != null) {
result[match[1]] = true;
match = tagRegEx.exec(aLabel);
}
return result;
},
'tags': function () {
return Clipperz.Async.callbacks("Record.label", [
MochiKit.Base.method(this, 'fullLabel'),
@@ -1173,10 +1141,9 @@ console.log("Record.hasPendingChanges RESULT", result);
'exportDirectLogins': function() {
var result;
var directLoginsObject = this.directLogins();
if (Object.keys(directLoginsObject).length == 0) {
if (MochiKit.Base.keys(directLoginsObject).length == 0) {
result = {};
} else {
var callbackObject = Object.keys(directLoginsObject).reduce(function(previous, current) {
@@ -1184,7 +1151,7 @@ console.log("Record.hasPendingChanges RESULT", result);
return previous;
}, {});
result = Clipperz.Async.collectResults("Record.exportDirectLogins",callbackObject,{trace:false})();
result = Clipperz.Async.collectResults("Record.exportDirectLogins", callbackObject,{trace:false})();
}
return result;
@@ -1202,20 +1169,20 @@ console.log("Record.hasPendingChanges RESULT", result);
currentVersion = {};
directLogins = {};
deferredResult = new Clipperz.Async.Deferred('Record.export', {trace:false});
deferredResult.addMethod(this,'getCurrentRecordVersion');
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.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.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() {
deferredResult.addMethod(this, function() {
return {
'label': label,
'data': data,
@@ -1267,3 +1234,34 @@ Clipperz.PM.DataModel.Record.isRegularTag = function (aTag) {
Clipperz.PM.DataModel.Record.regExpForSearch = function (aSearch) {
return new RegExp(aSearch.replace(/[^A-Za-z0-9]/g, '\\$&'), 'i');
};
Clipperz.PM.DataModel.Record.tagRegExp = new RegExp('\\' + Clipperz.PM.DataModel.Record.tagChar + '(' + Clipperz.PM.DataModel.Record.specialTagChar + '?\\w+)', 'g');
Clipperz.PM.DataModel.Record.trimSpacesRegExp = new RegExp('^\\s+|\\s+$', 'g');
Clipperz.PM.DataModel.Record.extractLabelFromFullLabel = function (aValue) {
var value;
value = aValue;
value = value.replace(Clipperz.PM.DataModel.Record.tagRegExp, '');
value = value.replace(Clipperz.PM.DataModel.Record.trimSpacesRegExp, '');
return value;
};
Clipperz.PM.DataModel.Record.extractTagsFromFullLabel = function (aLabel) {
var tagRegEx;
var result;
var match;
result = {};
tagRegEx = Clipperz.PM.DataModel.Record.tagRegExp;
match = tagRegEx.exec(aLabel);
while (match != null) {
result[match[1]] = true;
match = tagRegEx.exec(aLabel);
}
return result;
};

View File

@@ -30,18 +30,77 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataExportClass = React.createClass({
// featureSet: React.PropTypes.oneOf(['FULL', 'EXPIRED', 'TRIAL']).isRequired,
// 'level': React.PropTypes.oneOf(['hide', 'info', 'warning', 'error']).isRequired
},
/*
jsonExport: function () {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'export', 'json');
},
htmlExport: function () {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'export', 'html');
},
*/
isFeatureEnabled: function (aValue) {
return (this.props['features'].indexOf(aValue) > -1);
},
handleDownloadOfflineCopyLink: function (anEvent) {
if (this.isFeatureEnabled('OFFLINE_COPY')) {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'downloadOfflineCopy');
}
},
handleExportLink: function () {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'downloadExport');
},
//=========================================================================
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")
React.DOM.div({'className': 'content'}, [
React.DOM.ul({}, [
React.DOM.li({}, [
React.DOM.h3({}, "Offline copy"),
React.DOM.div({'className':'description'}, [
React.DOM.p({}, "Download a read-only portable version of Clipperz. Very convenient when no Internet connection is available."),
React.DOM.p({}, "An offline copy is just a single HTML file that contains both the whole Clipperz web application and your encrypted data."),
React.DOM.p({}, "It is as secure as the hosted Clipperz service since they both share the same code and security architecture.")
]),
React.DOM.a({'className':'button', 'onClick':this.handleDownloadOfflineCopyLink}, "download offline copy")
]),
React.DOM.li({}, [
React.DOM.h3({}, "HTML + JSON"),
React.DOM.div({'className':'description'}, [
React.DOM.p({}, "Download a printer-friendly HTML file that lists the content of all your cards."),
React.DOM.p({}, "This same file also contains all your data in JSON format."),
React.DOM.p({}, "Beware: all data are unencrypted! Therefore make sure to properly store and manage this file.")
]),
React.DOM.a({'className':'button', 'onClick':this.handleExportLink}, "download HTML+JSON")
]),
/*
React.DOM.li({}, [
React.DOM.h3({}, "Printing"),
React.DOM.div({'className':'description'}, [
React.DOM.p({}, "Click on the button below to open a new window displaying all your cards in a printable format."),
React.DOM.p({}, "If you are going to print for backup purposes, please consider the safer option provided by the “offline copy”.")
]),
React.DOM.a({'className':'button', 'onClick':this.htmlExport}, "HTML")
]),
React.DOM.li({}, [
React.DOM.h3({}, "Exporting to JSON"),
React.DOM.div({'className':'description'}, [
React.DOM.p({}, "JSON enables a “lossless” export of your cards. All the information will be preserved, including direct login configurations."),
React.DOM.p({}, "This custom format its quite convenient if you need to move some of all of your cards to a different Clipperz account. Or if you want to restore a card that has been accidentally deleted."),
React.DOM.p({}, "Click on the button below to start the export process.")
]),
React.DOM.a({'className':'button', 'onClick':this.jsonExport}, "JSON"),
])
*/
])
])
]);
},

View File

@@ -78,19 +78,21 @@ Clipperz.PM.UI.Components.ExtraFeatures.DeleteAccountClass = React.createClass({
return React.DOM.div({className:'extraFeature deleteAccount'}, [
React.DOM.h1({}, "Delete Account"),
React.DOM.form({'key':'form', 'className':'deleteAccountForm', 'onChange': this.handleFormChange, 'onSubmit':this.handleDeleteAccount}, [
React.DOM.div({'key':'fields'},[
React.DOM.label({'key':'username-label', 'htmlFor' :'name'}, "username"),
React.DOM.input({'key':'username', 'className':this.state['username'], 'type':'text', 'name':'name', 'ref':'username', 'placeholder':"username", 'autoCapitalize':'none'}),
React.DOM.label({'key':'passphrase-label', 'autoFocus': 'true', 'htmlFor' :'passphrase'}, "passphrase"),
React.DOM.input({'key':'passphrase', 'className':this.state['passphrase'], 'type':'password', 'name':'passphrase', 'ref':'passphrase', 'placeholder':"passphrase"}),
React.DOM.p({}, [
React.DOM.input({'key':'confirm', 'className':'confirmCheckbox', 'type':'checkbox', 'name':'confirm', 'ref':'confirm'}),
React.DOM.span({}, "I understand that all my data will be deleted and that this action is irreversible.")
React.DOM.div({'className': 'content'}, [
React.DOM.form({'key':'form', 'className':'deleteAccountForm', 'onChange': this.handleFormChange, 'onSubmit':this.handleDeleteAccount}, [
React.DOM.div({'key':'fields'},[
React.DOM.label({'key':'username-label', 'htmlFor' :'name'}, "username"),
React.DOM.input({'key':'username', 'className':this.state['username'], 'type':'text', 'name':'name', 'ref':'username', 'placeholder':"username", 'autoCapitalize':'none'}),
React.DOM.label({'key':'passphrase-label', 'autoFocus': 'true', 'htmlFor' :'passphrase'}, "passphrase"),
React.DOM.input({'key':'passphrase', 'className':this.state['passphrase'], 'type':'password', 'name':'passphrase', 'ref':'passphrase', 'placeholder':"passphrase"}),
React.DOM.p({}, [
React.DOM.input({'key':'confirm', 'className':'confirmCheckbox', 'type':'checkbox', 'name':'confirm', 'ref':'confirm'}),
React.DOM.span({}, "I understand that all my data will be deleted and that this action is irreversible.")
]),
]),
]),
React.DOM.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableDeleteAccountButton(), 'className':'button'}, "Delete my account")
//~ React.DOM.div({ref: 'errorMessage', className: 'errorMessage', style: {visibility: errorVisibility} }, this.state.error)
React.DOM.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableDeleteAccountButton(), 'className':'button'}, "Delete my account")
//~ React.DOM.div({ref: 'errorMessage', className: 'errorMessage', style: {visibility: errorVisibility} }, this.state.error)
])
])
]);
},

View File

@@ -36,7 +36,9 @@ Clipperz.PM.UI.Components.ExtraFeatures.DevicePINClass = React.createClass({
render: function () {
return React.DOM.div({className:'extraFeature devicePIN'}, [
React.DOM.h1({}, "Device PIN"),
React.DOM.h3({}, this.props['PIN'])
React.DOM.div({'className': 'content'}, [
React.DOM.h3({}, this.props['PIN'])
])
]);
},

View File

@@ -101,27 +101,29 @@ Clipperz.PM.UI.Components.ExtraFeatures.PassphraseClass = React.createClass({
render: function () {
return React.DOM.div({className:'extraFeature passphrase'}, [
React.DOM.h1({}, "Change Passphrase"),
React.DOM.form({'key':'form', 'className':'changePassphraseForm', 'onChange': this.handleFormChange, 'onSubmit':this.handleChangePassphrase}, [
React.DOM.div({'key':'fields'},[
React.DOM.label({'key':'username-label', 'htmlFor' :'name'}, "username"),
React.DOM.input({'key':'username', 'className':this.state['username'], 'type':'text', 'name':'name', 'ref':'username', 'placeholder':"username", 'autoCapitalize':'none'}),
React.DOM.div({'className': 'content'}, [
React.DOM.form({'key':'form', 'className':'changePassphraseForm', 'onChange': this.handleFormChange, 'onSubmit':this.handleChangePassphrase}, [
React.DOM.div({'key':'fields'},[
React.DOM.label({'key':'username-label', 'htmlFor' :'name'}, "username"),
React.DOM.input({'key':'username', 'className':this.state['username'], 'type':'text', 'name':'name', 'ref':'username', 'placeholder':"username", 'autoCapitalize':'none'}),
React.DOM.label({'key':'old-passphrase-label', 'htmlFor' :'old-passphrase'}, "old passphrase"),
React.DOM.input({'key':'old-passphrase', 'className':this.state['old-passphrase'], 'type':'password', 'name':'old-passphrase', 'ref':'old-passphrase', 'placeholder':"old passphrase"}),
React.DOM.label({'key':'old-passphrase-label', 'htmlFor' :'old-passphrase'}, "old passphrase"),
React.DOM.input({'key':'old-passphrase', 'className':this.state['old-passphrase'], 'type':'password', 'name':'old-passphrase', 'ref':'old-passphrase', 'placeholder':"old passphrase"}),
React.DOM.label({'key':'new-passphrase-label', 'autoFocus': 'true', 'htmlFor' :'new-passphrase'}, "new passphrase"),
React.DOM.input({'key':'new-passphrase', 'className':this.state['new-passphrase'], 'type':'password', 'name':'new-passphrase', 'ref':'new-passphrase', 'placeholder':"new passphrase"}),
React.DOM.label({'key':'new-passphrase-label', 'autoFocus': 'true', 'htmlFor' :'new-passphrase'}, "new passphrase"),
React.DOM.input({'key':'new-passphrase', 'className':this.state['new-passphrase'], 'type':'password', 'name':'new-passphrase', 'ref':'new-passphrase', 'placeholder':"new passphrase"}),
React.DOM.label({'key':'confirm-new-passphrase-label', 'htmlFor' :'confirm-new-passphrase'}, "confirm new passphrase"),
React.DOM.input({'key':'confirm-new-passphrase', 'className':this.state['confirm-new-passphrase'], 'type':'password', 'name':'confirm-new-passphrase', 'ref':'confirm-new-passphrase', 'placeholder':"confirm new passphrase"}),
React.DOM.label({'key':'confirm-new-passphrase-label', 'htmlFor' :'confirm-new-passphrase'}, "confirm new passphrase"),
React.DOM.input({'key':'confirm-new-passphrase', 'className':this.state['confirm-new-passphrase'], 'type':'password', 'name':'confirm-new-passphrase', 'ref':'confirm-new-passphrase', 'placeholder':"confirm new passphrase"}),
React.DOM.p({}, [
React.DOM.input({'key':'confirm', 'className':'confirmCheckbox', 'type':'checkbox', 'name':'confirm', 'ref':'confirm'}),
React.DOM.span({}, "I understand that Clipperz will not be able to recover a lost passphrase.")
React.DOM.p({}, [
React.DOM.input({'key':'confirm', 'className':'confirmCheckbox', 'type':'checkbox', 'name':'confirm', 'ref':'confirm'}),
React.DOM.span({}, "I understand that Clipperz will not be able to recover a lost passphrase.")
]),
]),
]),
React.DOM.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableChangePassphraseButton(), 'className':'button'}, "Change passphrase"),
]),
React.DOM.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableChangePassphraseButton(), 'className':'button'}, "Change passphrase"),
])
])
]);
},

View File

@@ -53,11 +53,15 @@ Clipperz.Base.extend(Clipperz.PM.UI.Components.Overlay, Object, {
//-------------------------------------------------------------------------
'show': function (aMessage, showMask) {
'show': function (aMessage, showMask, showProgress) {
if (showMask === true) {
this.showMask();
}
if (showProgress === true) {
this.showProgressBar();
}
this.resetStatus();
this.setMessage(aMessage);
MochiKit.DOM.removeElementClass(this.element(), 'ios-overlay-hide');
@@ -66,6 +70,7 @@ Clipperz.Base.extend(Clipperz.PM.UI.Components.Overlay, Object, {
'done': function (aMessage, aDelayBeforeHiding) {
this.hideMask();
this.hideProgressBar();
this.completed(this.showDoneIcon, aMessage, aDelayBeforeHiding);
},
@@ -114,6 +119,7 @@ Clipperz.Base.extend(Clipperz.PM.UI.Components.Overlay, Object, {
'hide': function () {
var element = this.element();
this.hideProgressBar();
MochiKit.DOM.removeElementClass(element, 'ios-overlay-show');
MochiKit.DOM.addElementClass(element, 'ios-overlay-hide');
return MochiKit.Async.callLater(1, MochiKit.Style.hideElement, element);
@@ -133,6 +139,21 @@ Clipperz.Base.extend(Clipperz.PM.UI.Components.Overlay, Object, {
//-------------------------------------------------------------------------
'showProgressBar': function () {
MochiKit.Style.showElement(this.getElement('progressBar'));
},
'hideProgressBar': function () {
MochiKit.Style.hideElement(this.getElement('progressBar'));
},
'updateProgress': function (aProgressPercentage) {
MochiKit.Style.setElementDimensions(this.getElement('progress'), {'w': aProgressPercentage}, '%');
//console.log("OVERLAY - updating progress: " + aProgressPercentage + "%");
},
//-------------------------------------------------------------------------
'defaultDelay': function () {
return this._defaultDelay;
},

View File

@@ -70,9 +70,13 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
//=========================================================================
showExtraFeatureComponent: function (aComponentName) {
toggleExtraFeatureComponent: function (aComponentName) {
return MochiKit.Base.bind(function () {
this.showExtraFeatureContent(Clipperz.PM.UI.Components.ExtraFeatures[aComponentName], aComponentName);
if (this.state['extraFeatureComponentName'] != aComponentName) {
this.showExtraFeatureContent(Clipperz.PM.UI.Components.ExtraFeatures[aComponentName], aComponentName);
} else {
this.hideExtraFeatureContent();
}
}, this);
},
@@ -118,7 +122,7 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
React.DOM.li({'key':'account', 'className':this.state['index']['account'] ? 'open' : 'closed'}, [
React.DOM.h1({'key':'accountH1', 'onClick':this.toggleIndexState('account')}, "Account"),
React.DOM.ul({'key':'accountUL'}, [
React.DOM.li({'key':'account_1', 'onClick':this.showExtraFeatureComponent('Passphrase'), 'className':(this.state['extraFeatureComponentName'] == 'Passphrase') ? 'selected' : ''}, [
React.DOM.li({'key':'account_1', 'onClick':this.toggleExtraFeatureComponent('Passphrase'), 'className':(this.state['extraFeatureComponentName'] == 'Passphrase') ? 'selected' : ''}, [
React.DOM.h2({'key':'account_1_h2'}, "Passphrase"),
React.DOM.div({'key':'account_1_div'}, [
React.DOM.p({'key':'account_1_p'}, "Change your account passphrase.")
@@ -130,7 +134,7 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
React.DOM.p({}, "")
])
]),
React.DOM.li({'key':'account_3', 'onClick':this.showExtraFeatureComponent('DevicePIN')}, [
React.DOM.li({'key':'account_3', 'onClick':this.toggleExtraFeatureComponent('DevicePIN')}, [
React.DOM.h2({}, "Device PIN"),
React.DOM.div({}, [
React.DOM.p({}, "Configure a PIN that will allow to get access to your cards, but only on this device.")
@@ -142,7 +146,7 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
React.DOM.p({}, "")
])
]),
React.DOM.li({'key':'account_5', 'onClick':this.showExtraFeatureComponent('DeleteAccount'), 'className':(this.state['extraFeatureComponentName'] == 'DeleteAccount') ? 'selected' : ''}, [
React.DOM.li({'key':'account_5', 'onClick':this.toggleExtraFeatureComponent('DeleteAccount'), 'className':(this.state['extraFeatureComponentName'] == 'DeleteAccount') ? 'selected' : ''}, [
React.DOM.h2({}, "Delete account"),
React.DOM.div({}, [
React.DOM.p({}, "Delete your account for good.")
@@ -182,29 +186,29 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
React.DOM.li({'key':'data', 'className':this.state['index']['data'] ? 'open' : 'closed'}, [
React.DOM.h1({'onClick':this.toggleIndexState('data')}, "Data"),
React.DOM.ul({'key':'data'}, [
React.DOM.li({'key':'data_1'}, [
React.DOM.h2({}, "Offline copy"),
React.DOM.div({}, [
React.DOM.p({}, "With just one click you can dump all your encrypted data from Clipperz servers to your hard disk and create a read-only offline version of Clipperz to be used when you are not connected to the Internet."),
React.DOM.a({'className':Clipperz.PM.UI.Components.classNames(offlineCopyButtonClasses), 'onClick':this.handleDownloadOfflineCopyLink}, "Download")
])
]),
// React.DOM.li({'key':'data_1', 'className':'offlineCopy'}, [
// React.DOM.h2({}, "Offline copy"),
// React.DOM.div({}, [
// React.DOM.p({}, "With just one click you can dump all your encrypted data from Clipperz servers to your hard disk and create a read-only offline version of Clipperz to be used when you are not connected to the Internet."),
// React.DOM.a({'className':Clipperz.PM.UI.Components.classNames(offlineCopyButtonClasses), 'onClick':this.handleDownloadOfflineCopyLink}, "Download")
// ])
// ]),
React.DOM.li({'key':'data_2'}, [
React.DOM.h2({}, "Import"),
React.DOM.div({}, [
React.DOM.p({}, "")
React.DOM.p({}, "CSV, JSON, …")
])
]),
React.DOM.li({'key':'data_3', 'onClick':this.showExtraFeatureComponent('DataExport')}, [
React.DOM.li({'key':'data_3', 'onClick':this.toggleExtraFeatureComponent('DataExport'), 'className':(this.state['extraFeatureComponentName'] == 'DataExport') ? 'selected' : ''}, [
React.DOM.h2({}, "Export"),
React.DOM.div({}, [
React.DOM.p({}, "")
React.DOM.p({}, "Offline copy, printable version, JSON, …")
])
]),
React.DOM.li({'key':'data_4'}, [
React.DOM.h2({}, "Sharing"),
React.DOM.div({}, [
React.DOM.p({}, "")
React.DOM.p({}, "Securely share cards with other users")
])
])
])
@@ -229,17 +233,20 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
render: function () {
//console.log("ExtraFeaturesPanel props", this.props);
var isOpen = (this.props['settingsPanelStatus'] == 'OPEN');
var isFullyOpen = isOpen && this.state['isFullyOpen'];
var classes = {
'panel': true,
'right': true,
'open': this.props['settingsPanelStatus'] == 'OPEN',
'fullOpen': this.state['isFullyOpen']
'open': isOpen,
'fullOpen': isFullyOpen
}
return React.DOM.div({'key':'extraFeaturesPanel', 'id':'extraFeaturesPanel', 'className':Clipperz.PM.UI.Components.classNames(classes)}, [
this.renderIndex(),
this.renderContent(),
// (this.props['settingsPanelStatus'] == 'OPEN') ? this.renderContent() : null,
]);
}

View File

@@ -21,87 +21,111 @@ refer to http://www.clipperz.com.
*/
"use strict";
Clipperz.Base.module('Clipperz.PM.UI');
// https://github.com/eligrey/FileSaver.js
// https://github.com/eligrey/Blob.js
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._processedRecords = 0;
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;"+
"}"+
"";
this._style =
"body {" +
"font-family: 'Dejavu Sans', monospace;" +
"margin: 0px;" +
"}" +
"header {" +
"padding: 10px;" +
"border-bottom: 2px solid black;" +
"}" +
"h1 {" +
"margin: 0px;" +
"}" +
"h2 {" +
"margin: 0px;" +
"padding-top: 10px;" +
"}" +
"h3 {" +
"margin: 0px;" +
"}" +
"h5 {" +
"margin: 0px;" +
"color: gray;" +
"}" +
"ul {" +
"margin: 0px;" +
"padding: 0px;" +
"}" +
"div > ul > li {" +
"border-bottom: 1px solid black;" +
"padding: 10px;" +
"}" +
"div > ul > li.archived {" +
"background-color: #ddd;" +
"}" +
"ul > li > ul > li {" +
"font-size: 9pt;" +
"display: inline-block;" +
"}" +
"ul > li > ul > li:after {" +
"content: \",\";" +
"padding-right: 5px;" +
"}" +
"ul > li > ul > li:last-child:after {" +
"content: \"\";" +
"padding-right: 0px;" +
"}" +
"dl {" +
"}" +
"dt {" +
"color: gray;" +
"font-size: 9pt;" +
"}" +
"dd {" +
"margin: 0px;" +
"margin-bottom: 5px;" +
"padding-left: 10px;" +
"}" +
"div > div {" +
"background-color: black;" +
"color: white;" +
"padding: 10px;" +
"}" +
"textarea {" +
"width: 100%;" +
"height: 200px;" +
"}" +
"@media print {" +
"div > div, header > div {" +
"display: none !important;" +
"}" +
"ul > li {" +
"page-break-inside: avoid;" +
"} " +
"}" +
"";
return this;
}
@@ -114,188 +138,141 @@ MochiKit.Base.update(Clipperz.PM.UI.ExportController.prototype, {
//-----------------------------------------------------------------------------
'type': function () {
return this._type;
},
'recordsInfo': function () {
return this._recordsInfo;
},
'target': function () {
return this._target;
//=============================================================================
'reportRecordExport': function (aRecordData) {
var percentage;
var exportedCardsCount;
var totalCardsToExport;
this._processedRecords = this._processedRecords + 1;
exportedCardsCount = this._processedRecords;
totalCardsToExport = this.recordsInfo().length;
percentage = Math.round(100 * exportedCardsCount / totalCardsToExport);
//console.log("PROCESSING " + exportedCardsCount + "/" + totalCardsToExport + " - " + percentage + "%");
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'updateProgress', percentage);
return MochiKit.Async.succeed(aRecordData);
},
//=============================================================================
'setWindowTitle': function (aWindow, aTitle) {
aWindow.document.title = aTitle;
},
'setWindowBody': function (aWindow, anHTML) {
aWindow.document.body.innerHTML = anHTML;
'renderCardToHtml': function (jsonCardData) {
var label = Clipperz.PM.DataModel.Record.extractLabelFromFullLabel(jsonCardData.label);
var allTags = MochiKit.Base.keys(Clipperz.PM.DataModel.Record.extractTagsFromFullLabel(jsonCardData.label));
var regularTags = MochiKit.Base.filter(Clipperz.PM.DataModel.Record.isRegularTag, allTags);
var isArchived = MochiKit.Iter.some(allTags, MochiKit.Base.partial(MochiKit.Base.objEqual, Clipperz.PM.DataModel.Record.archivedTag));
return MochiKit.DOM.LI({'class': isArchived ? 'archived' : ""},
MochiKit.DOM.H2({}, label),
(regularTags.length > 0) ? MochiKit.DOM.UL({}, MochiKit.Base.map(function (tag) { return MochiKit.DOM.LI({}, tag);}, regularTags)): null,
MochiKit.DOM.DIV({},
MochiKit.DOM.DL({},
MochiKit.Base.map(function(key) {
return [
MochiKit.DOM.DT(jsonCardData.currentVersion.fields[key].label),
MochiKit.DOM.DD(jsonCardData.currentVersion.fields[key].value),
];
}, MochiKit.Base.keys(jsonCardData.currentVersion.fields))
)
),
jsonCardData.data.notes ? MochiKit.DOM.P({}, jsonCardData.data.notes) : null
);
},
//=============================================================================
'renderToHtml': function (jsonData) {
var title;
var style;
var date;
var body;
'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'},"") )
title = "Clipperz data";
style = this._style;
date = "dd/mm/yyyy";
body = MochiKit.DOM.DIV({},
MochiKit.DOM.HEADER({},
MochiKit.DOM.H1({}, "Your data on Clipperz"),
MochiKit.DOM.H5({}, "Export date: " + date),
MochiKit.DOM.DIV({},
MochiKit.DOM.P({}, "Security warning - This file lists the content of all your cards in a printer-friendly format. At the very bottom, the same content is also available in JSON format."),
MochiKit.DOM.P({}, "Beware: all data are unencrypted! Therefore make sure to properly store and manage this file. We recommend to delete it as soon as it is no longer needed."),
MochiKit.DOM.P({}, "If you are going to print its content on paper, store the printout in a safe and private place!"),
MochiKit.DOM.P({}, "And, if you need to access your data when no Internet connection is available, please consider the much safer option of creating an offline copy.")
)
),
MochiKit.DOM.UL({}, MochiKit.Base.map(this.renderCardToHtml, jsonData)),
MochiKit.DOM.DIV({},
MochiKit.DOM.H3({}, "JSON content"),
MochiKit.DOM.DIV({},
MochiKit.DOM.P({}, "Instructions on how to use JSON content"),
MochiKit.DOM.P({}, "The JSON version of your data may be useful if you want to move the whole content of your Clipperz account to a new Clipperz account or recover a card that has been accidentally deleted. Just follow these instructions:"),
MochiKit.DOM.OL({},
MochiKit.DOM.LI({}, "Login to your Clipperz account and go to \"Data > Import\"."),
MochiKit.DOM.LI({}, "Select the JSON option."),
MochiKit.DOM.LI({}, "Copy and paste the JSON content in the form.")
),
MochiKit.DOM.P({}, "Of course, the unencrypted JSON content won't be transmitted to the Clipperz server.")
),
MochiKit.DOM.TEXTAREA({}, Clipperz.Base.serializeJSON(jsonData)),
MochiKit.DOM.FOOTER({},
MochiKit.DOM.P({},
"This file has been downloaded from clipperz.is, a service by Clipperz Srl. - ",
MochiKit.DOM.A({'href':'https://clipperz.is/terms_service/'}, "Terms of service"),
" - ",
MochiKit.DOM.A({'href':'https://clipperz.is/privacy_policy/'}, "Privacy policy")
),
MochiKit.DOM.H4({}, "Clipperz - keep it to yourself")
)
)
);
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));
return '<html><head><title>' + title + '</title><style type="text/css">' + style + '</style></head><body>' + MochiKit.DOM.toHTML(body) + '</body></html>';
},
//-----------------------------------------------------------------------------
'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))
);
//----------------------------------------------------------------------------
'saveResult': function (exportedJSON) {
var blob;
var sortedJSON;
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,
"<h3>Error</h3>"+
"<p>The following error occured while exporting your data:</p>"+
"<code>"+errorMessage+"</code>"
);
sortedJSON = MochiKit.Iter.sorted(exportedJSON, function(a,b) { return a.label.toUpperCase().localeCompare(b.label.toUpperCase()); } );
blob = new Blob([this.renderToHtml(sortedJSON)], {type: "text/html;charset=utf-8"});
saveAs(blob, "clipperz_data.html");
},
//=============================================================================
'runExportJSON': function (aWindow) {
'run': function () {
var deferredResult;
var exportedRecords;
var totalRecords = this.recordsInfo().length;
exportedRecords = 0;
var self = this;
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 = new Clipperz.Async.Deferred("ExportController.run", {trace:false});
deferredResult.addCallback(MochiKit.Base.map, function(recordIn) {
var innerDeferredResult;
innerDeferredResult = new Clipperz.Async.Deferred("ExportController.run__exportRecord", {trace:false});
innerDeferredResult.addMethod(recordIn._rowObject, 'export');
innerDeferredResult.addMethod(self, 'reportRecordExport');
innerDeferredResult.callback();
return innerDeferredResult;
});
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();
deferredResult.addMethod(this, 'saveResult');
deferredResult.callback(this.recordsInfo());
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();
};

View File

@@ -63,7 +63,9 @@ Clipperz.PM.UI.MainController = function() {
this.registerForNotificationCenterEvents([
'doLogin', 'registerNewUser', 'showRegistrationForm', 'goBack',
'changePassphrase', 'deleteAccount',
'export',
// 'export',
'downloadExport',
'updateProgress',
'toggleSelectionPanel', 'toggleSettingsPanel',
'matchMediaQuery', 'unmatchMediaQuery',
'selectAllCards', 'selectRecentCards', 'search', 'tagSelected', 'selectUntaggedCards',
@@ -106,6 +108,10 @@ MochiKit.Base.update(Clipperz.PM.UI.MainController.prototype, {
return this._overlay;
},
updateProgress_handler: function (aProgressPercentage) {
this.overlay().updateProgress(aProgressPercentage);
},
loginForm: function () {
return this._loginForm;
},
@@ -1240,8 +1246,24 @@ console.log("THE BROWSER IS OFFLINE");
//----------------------------------------------------------------------------
export_handler: function(exportType) {
return Clipperz.PM.UI.ExportController.exportJSON( this.recordsInfo(), exportType );
// export_handler: function(exportType) {
// return Clipperz.PM.UI.ExportController.exportJSON( this.recordsInfo(), exportType );
// },
downloadExport_handler: function () {
var exportController;
var deferredResult;
exportController = new Clipperz.PM.UI.ExportController({'recordsInfo': this.recordsInfo()});
deferredResult = new Clipperz.Async.Deferred("MainController.downloadExport_handler", {trace: false});
deferredResult.addMethod(this.overlay(), 'show', "exporting …", true, true);
// deferredResult.addCallback(MochiKit.Signal.signal, Clipperz.Signal.NotificationCenter, 'toggleSettingsPanel');
deferredResult.addMethod(exportController, 'run');
deferredResult.addMethod(this.overlay(), 'done', "", 1);
deferredResult.callback();
return deferredResult;
},
//----------------------------------------------------------------------------