From edeb859746e2581c80d397c5ac333fc84f13a45a Mon Sep 17 00:00:00 2001 From: Giulio Cesare Solaroli Date: Tue, 14 Apr 2015 11:07:10 +0200 Subject: [PATCH 1/6] Merged and cleaned-up features implemented by Dario - delete account - change passphrase --- frontend/delta/css/clipperz.css | 180 +++++++++++++----- .../delta/js/Clipperz/PM/DataModel/User.js | 10 - .../Components/ExtraFeatures/DeleteAccount.js | 41 +--- .../UI/Components/ExtraFeatures/Passphrase.js | 115 +++++------ .../js/Clipperz/PM/UI/Components/Overlay.js | 4 +- .../Components/Panels/ExtraFeaturesPanel.js | 31 ++- .../delta/js/Clipperz/PM/UI/MainController.js | 19 +- frontend/delta/scss/core/layout.scss | 2 +- frontend/delta/scss/style/dialogBox.scss | 4 + frontend/delta/scss/style/settingsPanel.scss | 120 +++++++++--- 10 files changed, 330 insertions(+), 196 deletions(-) diff --git a/frontend/delta/css/clipperz.css b/frontend/delta/css/clipperz.css index ddd4ad0..2c6fab1 100644 --- a/frontend/delta/css/clipperz.css +++ b/frontend/delta/css/clipperz.css @@ -128,6 +128,7 @@ http://jonibologna.com/flexbox-cheatsheet/ -ms-transform: rotate(0deg) translate(0, 0); -o-transform: rotate(0deg) translate(0, 0); transform: rotate(0deg) translate(0, 0); } + 100% { -webkit-transform: rotate(359deg) translate(0, 0); -moz-transform: rotate(359deg) translate(0, 0); @@ -141,6 +142,7 @@ http://jonibologna.com/flexbox-cheatsheet/ -ms-transform: rotate(0deg) translate(0, 0); -o-transform: rotate(0deg) translate(0, 0); transform: rotate(0deg) translate(0, 0); } + 100% { -webkit-transform: rotate(359deg) translate(0, 0); -moz-transform: rotate(359deg) translate(0, 0); @@ -154,6 +156,7 @@ http://jonibologna.com/flexbox-cheatsheet/ -ms-transform: rotate(0deg) translate(0, 0); -o-transform: rotate(0deg) translate(0, 0); transform: rotate(0deg) translate(0, 0); } + 100% { -webkit-transform: rotate(359deg) translate(0, 0); -moz-transform: rotate(359deg) translate(0, 0); @@ -167,6 +170,7 @@ http://jonibologna.com/flexbox-cheatsheet/ -ms-transform: rotate(0deg) translate(0, 0); -o-transform: rotate(0deg) translate(0, 0); transform: rotate(0deg) translate(0, 0); } + 100% { -webkit-transform: rotate(359deg) translate(0, 0); -moz-transform: rotate(359deg) translate(0, 0); @@ -469,61 +473,73 @@ div.overlay { @-webkit-keyframes overlay-spin { from { opacity: 1; } + to { opacity: 0.25; } } @-moz-keyframes overlay-spin { from { opacity: 1; } + to { opacity: 0.25; } } @-ms-keyframes overlay-spin { from { opacity: 1; } + to { opacity: 0.25; } } @keyframes overlay-spin { from { opacity: 1; } + to { opacity: 0.25; } } @-webkit-keyframes ios-overlay-show { 0% { opacity: 0; } + 100% { opacity: 1; } } @-moz-keyframes ios-overlay-show { 0% { opacity: 0; } + 100% { opacity: 1; } } @-ms-keyframes ios-overlay-show { 0% { opacity: 0; } + 100% { opacity: 1; } } @keyframes ios-overlay-show { 0% { opacity: 0; } + 100% { opacity: 1; } } @-webkit-keyframes ios-overlay-hide { 0% { opacity: 1; } + 100% { opacity: 0; } } @-moz-keyframes ios-overlay-hide { 0% { opacity: 1; } + 100% { opacity: 0; } } @-ms-keyframes ios-overlay-hide { 0% { opacity: 1; } + 100% { opacity: 0; } } @keyframes ios-overlay-hide { 0% { opacity: 1; } + 100% { opacity: 0; } } /* @@ -1206,7 +1222,7 @@ div.dialogBox { z-index: 10; background-color: rgba(0, 0, 0, 0.5); } div.dialogBox .mask { - z-index: 12; } + z-index: 25; } div.dialogBox div.dialog { -webkit-box-flex: none; -webkit-flex: none; @@ -1494,11 +1510,11 @@ div.dialogBox { flex: 1; font-size: 8pt; } #loginPage div.loginForm footer .applicationVersion span { - color: #999; } + color: #999999; } #loginPage div.loginForm footer .applicationVersion span:after { content: ":"; } #loginPage div.loginForm footer .applicationVersion a { - color: #999; + color: #999999; text-decoration: none; padding-left: 5px; font-weight: bold; } @@ -1983,20 +1999,22 @@ span.count { border-top: 1px solid white; } #extraFeaturesPanel .extraFeatureIndex > div > ul > li { border-bottom: 1px solid white; } - #extraFeaturesPanel .extraFeatureIndex > div > ul > li > ul { - padding-left: 10px; } + #extraFeaturesPanel .extraFeatureIndex > div > ul > li > h1 { + cursor: pointer; + font-size: 16pt; + padding: 10px; } #extraFeaturesPanel .extraFeatureIndex > div > ul > li.closed > ul { display: none; visibility: hidden; } #extraFeaturesPanel .extraFeatureIndex > div ul li > ul > li { padding: 10px; - padding-right: 0px; } + padding-left: 20px; + padding-right: 0px; + cursor: pointer; } + #extraFeaturesPanel .extraFeatureIndex > div ul li > ul > li.selected { + background-color: #333; } #extraFeaturesPanel .extraFeatureIndex > div ul li > ul > li > div { padding: 4px; } - #extraFeaturesPanel .extraFeatureIndex > div ul li h1 { - cursor: pointer; - font-size: 16pt; - padding: 10px; } #extraFeaturesPanel .extraFeatureIndex > div ul li h2 { font-weight: 300; font-size: 14pt; } @@ -2024,46 +2042,114 @@ span.count { #extraFeaturesPanel .extraFeatureIndex footer { font-size: 8pt; padding: 5px 5px 5px 5px; - border-top: 1px solid #999; } + border-top: 1px solid #999999; } #extraFeaturesPanel .extraFeatureIndex footer span { - color: #999; } + color: #999999; } #extraFeaturesPanel .extraFeatureIndex footer span:after { content: ":"; } #extraFeaturesPanel .extraFeatureIndex footer a { - color: #999; + color: #999999; text-decoration: none; padding-left: 5px; font-weight: bold; } #extraFeaturesPanel .extraFeatureContent { border-right: 1px solid #222; - color: white; } + color: white; + /* + .changePassphraseForm { + label { + display: block; + } + + input { + display: block; + } + } + + .deleteAccountForm { + margin-top: 1em; + + label { + display: block; + } + + input { + display: inline-block; + margin-right: 1em; + margin-bottom: 1em; + } + + .confirmCheckbox { + display: inline-block; + } + } + */ } #extraFeaturesPanel .extraFeatureContent header { display: none; } - #extraFeaturesPanel .extraFeatureContent .changePassphraseForm label { - display: block; } - #extraFeaturesPanel .extraFeatureContent .changePassphraseForm input { - display: block; } - #extraFeaturesPanel .extraFeatureContent .deleteAccountForm { - margin-top: 1em; } - #extraFeaturesPanel .extraFeatureContent .deleteAccountForm label { - display: block; } - #extraFeaturesPanel .extraFeatureContent .deleteAccountForm input { - display: inline-block; - margin-right: 1em; - margin-bottom: 1em; } - #extraFeaturesPanel .extraFeatureContent .deleteAccountForm .confirmCheckbox { - display: inline-block; } - #extraFeaturesPanel .extraFeatureContent form input.valid + .invalidMsg, #extraFeaturesPanel .extraFeatureContent form input.empty + .invalidMsg, #extraFeaturesPanel .extraFeatureContent form input:focus + .invalidMsg, #extraFeaturesPanel .extraFeatureContent form input.invalid:focus + .invalidMsg { - visibility: hidden; } - #extraFeaturesPanel .extraFeatureContent form input:focus { - border: 2px solid #ff9900; } - #extraFeaturesPanel .extraFeatureContent form input.valid:focus { - border: 2px solid #1863a1; } - #extraFeaturesPanel .extraFeatureContent form input.invalid + .invalidMsg { - visibility: visible; } - #extraFeaturesPanel .extraFeatureContent form .invalidMsg::before { - font-family: serif; - content: "\26A0 \0000a0"; } + #extraFeaturesPanel .extraFeatureContent .extraFeature { + padding: 20px; } + #extraFeaturesPanel .extraFeatureContent .extraFeature h1 { + font-size: 20pt; + padding-bottom: 20px; } + #extraFeaturesPanel .extraFeatureContent form label { + display: none; } + #extraFeaturesPanel .extraFeatureContent form input { + display: block; + font-size: 18pt; + margin-bottom: 8px; + padding: 6px 10px; + border: 0px solid white; + width: 350px; + color: black; } + #extraFeaturesPanel .extraFeatureContent form input.invalid { + border: 0px solid #ff9900; + color: gray; } + #extraFeaturesPanel .extraFeatureContent form p { + display: -webkit-box; + display: -webkit-flex; + display: -moz-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-direction: normal; + -webkit-box-orient: horizontal; + -webkit-flex-direction: row; + -moz-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; } + #extraFeaturesPanel .extraFeatureContent form p input { + width: 30px; + -webkit-box-flex: auto; + -webkit-flex: auto; + -moz-box-flex: auto; + -moz-flex: auto; + -ms-flex: auto; + flex: auto; } + #extraFeaturesPanel .extraFeatureContent form p span { + -webkit-box-flex: auto; + -webkit-flex: auto; + -moz-box-flex: auto; + -moz-flex: auto; + -ms-flex: auto; + flex: auto; + font-size: 12pt; } + #extraFeaturesPanel .extraFeatureContent form button { + font-family: "clipperz-font"; + color: white; + font-size: 14pt; + border: 0px; + margin-top: 20px; + padding: 6px 10px; + border: 1px solid white; + background-color: #ff9900; + -webkit-transition: background-color font-weight 0.2s linear; + -moz-transition: background-color font-weight 0.2s linear; + -o-transition: background-color font-weight 0.2s linear; + -ms-transition: background-color font-weight 0.2s linear; + transition: background-color font-weight 0.2s linear; } + #extraFeaturesPanel .extraFeatureContent form button:disabled { + font-weight: 100; + background-color: #c0c0c0; + cursor: default; } .mainPage.narrow #extraFeaturesPanel .extraFeatureContent header { display: block; @@ -2145,7 +2231,7 @@ div.cardList ul { padding-right: 0px; box-shadow: -4px 0px 3px -1px rgba(0, 0, 0, 0.2); } div.cardList ul li.archived { - background-color: #eee; + background-color: #eeeeee; color: #999; } div.cardList ul li .favicon { width: 48px; @@ -2231,7 +2317,7 @@ div.cardList.narrow { content: ""; } #cardDetailPage .view.archived, .cardDetail .view.archived { - background-color: #eee; } + background-color: #eeeeee; } #cardDetailPage .view .cardDetailToolbar, .cardDetail .view .cardDetailToolbar { background-color: #1863a1; color: white; } @@ -2425,7 +2511,7 @@ div.cardList.narrow { cursor: grab; cursor: -moz-grab; cursor: -webkit-grab; - background: repeating-linear-gradient(0deg, white, white 2px, #ddd 2px, #ddd 3px); + background: repeating-linear-gradient(0deg, white, white 2px, #dddddd 2px, #dddddd 3px); width: 28px; height: 20px; margin-left: 6px; @@ -2580,12 +2666,16 @@ div.dialog { -webkit-border-radius: 8px; -moz-border-radius: 8px; border-radius: 8px; + max-width: 70%; background-color: white; - padding: 30px; } + padding: 30px; + box-shadow: 4px 4px 6px 5px rgba(0, 0, 0, 0.3); } div.dialog h3.message { font-size: 18pt; font-weight: bold; - padding-bottom: 20px; } + padding-bottom: 20px; + white-space: pre-wrap; + word-wrap: break-word; } div.dialog div.answers div.button { -webkit-border-radius: 4; -moz-border-radius: 4; @@ -2656,5 +2746,3 @@ This configuration is now located in the first script included in the index_temp } } */ - -/*# sourceMappingURL=clipperz.css.map */ diff --git a/frontend/delta/js/Clipperz/PM/DataModel/User.js b/frontend/delta/js/Clipperz/PM/DataModel/User.js index 365dccf..7b10a2e 100644 --- a/frontend/delta/js/Clipperz/PM/DataModel/User.js +++ b/frontend/delta/js/Clipperz/PM/DataModel/User.js @@ -244,11 +244,7 @@ Clipperz.Base.extend(Clipperz.PM.DataModel.User, Object, { return deferredResult; }, - // TODO: test (taken straight from /beta) 'deleteAccount': function() { - -console.log("deleting account from user"); - var deferredResult; deferredResult = new MochiKit.Async.Deferred("User.deleteAccount", {trace: true}); @@ -257,15 +253,9 @@ console.log("deleting account from user"); deferredResult.callback(); return deferredResult; - - }, - // TODO: check (I have half of an idea what i'm doing) 'resetAllLocalData': function() { - -console.log("resetting all local data..."); - var deferredResult; deferredResult = new MochiKit.Async.Deferred("User.resetAllLocalData", {trace: true}); diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DeleteAccount.js b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DeleteAccount.js index a38c17d..3c71f0d 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DeleteAccount.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DeleteAccount.js @@ -36,7 +36,6 @@ Clipperz.PM.UI.Components.ExtraFeatures.DeleteAccountClass = React.createClass({ 'username': 'empty', 'passphrase': 'empty', 'confirm': '', - //~ 'error': '' }; }, @@ -44,36 +43,10 @@ Clipperz.PM.UI.Components.ExtraFeatures.DeleteAccountClass = React.createClass({ handleDeleteAccount: function(event) { event.preventDefault(); - - //~ if (this.refs['username'].getDOMNode().value != this.props.userInfo['username']) { - //~ this.setState({error: "Invalid username"}); - //~ return; - //~ } - //~ - //~ var deferredResult; - //~ - //~ deferredResult = new Clipperz.Async.Deferred("DeleteAccount.handleDeleteAccount", {trace: false}); - //~ deferredResult.addCallback(this.props.userInfo['checkPassphraseCallback'], this.refs['passphrase'].getDOMNode().value); - //~ deferredResult.addIf( - //~ [MochiKit.Base.partial(MochiKit.Signal.signal, Clipperz.Signal.NotificationCenter, 'deleteAccount')], - //~ [MochiKit.Base.bind(this.setState, this, {error: "Invalid password"})] - //~ ); - //~ - //~ deferredResult.callback(); - //~ - //~ return deferredResult; - MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'deleteAccount'); }, handleFormChange: function() { - - - //~ if (this.refs['username'].getDOMNode().value != this.props.userInfo['username']) { - //~ this.setState({error: "Invalid username"}); - //~ return; - //~ } - var deferredResult; deferredResult = new Clipperz.Async.Deferred("DeleteAccount.handleDeleteAccount", {trace: false}); @@ -92,14 +65,6 @@ Clipperz.PM.UI.Components.ExtraFeatures.DeleteAccountClass = React.createClass({ deferredResult.callback(); return deferredResult; - - - - //~ this.setState({ - //~ 'username': this.refs['username'].getDOMNode().value, - //~ 'passphrase': this.refs['passphrase'].getDOMNode().value, - //~ 'confirm': this.refs['confirm'].getDOMNode().checked, - //~ }); }, shouldEnableDeleteAccountButton: function() { @@ -116,11 +81,9 @@ Clipperz.PM.UI.Components.ExtraFeatures.DeleteAccountClass = React.createClass({ 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.span({'className': 'invalidMsg'},'Invalid 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.span({'className': 'invalidMsg'},'Invalid 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.") diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/Passphrase.js b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/Passphrase.js index aaa0512..a286d3a 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/Passphrase.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/Passphrase.js @@ -27,95 +27,100 @@ Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures'); Clipperz.PM.UI.Components.ExtraFeatures.PassphraseClass = React.createClass({ propTypes: { -// featureSet: React.PropTypes.oneOf(['FULL', 'EXPIRED', 'TRIAL']).isRequired, -// 'level': React.PropTypes.oneOf(['hide', 'info', 'warning', 'error']).isRequired }, getInitialState: function() { return { - 'username': '', - 'old-passphrase': '', - 'new-passphrase': '', - 'confirm-new-passphrase': '', - 'error': '' + 'username': 'empty', + 'old-passphrase': 'empty', + 'new-passphrase': 'empty', + 'confirm-new-passphrase': 'empty', + 'confirm': '', }; }, //========================================================================= - shouldEnableChangePassphraseButton: function() { - return ( - this.state['username'] && - this.state['old-passphrase'] && - this.state['new-passphrase'] && - this.state['confirm-new-passphrase'] && - (this.state['new-passphrase'] == this.state['confirm-new-passphrase']) - ); - }, - - handleFormChange: function() { - this.setState({ - 'username': this.refs['username'].getDOMNode().value, - 'old-passphrase': this.refs['old-passphrase'].getDOMNode().value, - 'new-passphrase': this.refs['new-passphrase'].getDOMNode().value, - 'confirm-new-passphrase': this.refs['confirm-new-passphrase'].getDOMNode().value - }); + resetForm: function () { + this.setState(this.getInitialState()); + + this.refs['username'].getDOMNode().value = ''; + this.refs['old-passphrase'].getDOMNode().value = ''; + this.refs['new-passphrase'].getDOMNode().value = ''; + this.refs['confirm-new-passphrase'].getDOMNode().value = ''; + this.refs['confirm'].getDOMNode().checked = false; }, handleChangePassphrase: function(event) { - event.preventDefault(); - - if (this.refs['username'].getDOMNode().value != this.props.userInfo['username']) { - this.setState({error: "Invalid username"}); - return; - } + var newPassphrase; + event.preventDefault(); + newPassphrase = this.refs['new-passphrase'].getDOMNode().value; + this.resetForm(); + + MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'changePassphrase', newPassphrase); + }, + + handleFormChange: function() { var deferredResult; - deferredResult = new Clipperz.Async.Deferred("Passphrase.handleChangePassphrase", {trace: false}); + deferredResult = new Clipperz.Async.Deferred("Passphrase.handleFormChange", {trace: false}); deferredResult.addCallback(this.props.userInfo['checkPassphraseCallback'], this.refs['old-passphrase'].getDOMNode().value); - deferredResult.addIf( - [ - MochiKit.Base.partial(MochiKit.Signal.signal, Clipperz.Signal.NotificationCenter, 'changePassphrase', this.refs['new-passphrase'].getDOMNode().value), - MochiKit.Base.method(this, function() { - this.refs['username'].getDOMNode().value = ''; - this.refs['old-passphrase'].getDOMNode().value = ''; - this.refs['new-passphrase'].getDOMNode().value = ''; - this.refs['confirm-new-passphrase'].getDOMNode().value = ''; - this.setState({'error': ''}); - }) - ], - [MochiKit.Base.bind(this.setState, this, {error: "Invalid password"})] - ); + deferredResult.addMethod(this, function(passCheck){ + var username = this.refs['username'].getDOMNode().value; + var oldPassphrase = this.refs['old-passphrase'].getDOMNode().value; + var newPassphrase = this.refs['new-passphrase'].getDOMNode().value; + var confirmNewPassphrase = this.refs['confirm-new-passphrase'].getDOMNode().value; + + this.setState({ + 'username': (username != '') ? [(username == this.props.userInfo['username']) ? 'valid' : 'invalid'] : 'empty', + 'old-passphrase': (oldPassphrase != '') ? [(passCheck) ? 'valid' : 'invalid'] : 'empty', + 'new-passphrase': (newPassphrase != '') ? 'valid' : 'empty', + 'confirm-new-passphrase': (confirmNewPassphrase != '') ? [(confirmNewPassphrase == newPassphrase) ? 'valid' : 'invalid'] : 'empty', + 'confirm': this.refs['confirm'].getDOMNode().checked, + }); + }); deferredResult.callback(); return deferredResult; - -// MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'changePassphrase', this.refs['new-passphrase'].getDOMNode().value); - + }, + + shouldEnableChangePassphraseButton: function() { + return ( + this.state['username'] == 'valid' && + this.state['old-passphrase'] == 'valid' && + this.state['new-passphrase'] == 'valid' && + this.state['confirm-new-passphrase'] == 'valid' && + this.state['confirm'] + ); }, //========================================================================= render: function () { - var errorVisibility = (this.state.error) ? 'visible' : 'hidden'; - 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', 'type':'text', 'name':'name', 'ref':'username', 'placeholder':"username", 'autoCapitalize':'none'}), + 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', 'type':'password', 'name':'old-passphrase', 'ref':'old-passphrase', 'placeholder':"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', 'type':'password', 'name':'new-passphrase', 'ref':'new-passphrase', 'placeholder':"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', 'type':'password', 'name':'confirm-new-passphrase', 'ref':'confirm-new-passphrase', 'placeholder':"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.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableChangePassphraseButton(), 'className':'button'}, "Change"), - React.DOM.div({ref: 'errorMessage', className: 'errorMessage', style: {visibility: errorVisibility} }, this.state.error) + React.DOM.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableChangePassphraseButton(), 'className':'button'}, "Change passphrase"), ]), ]); }, diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/Overlay.js b/frontend/delta/js/Clipperz/PM/UI/Components/Overlay.js index 1595da5..f9f6bcd 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/Overlay.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/Overlay.js @@ -109,14 +109,14 @@ Clipperz.Base.extend(Clipperz.PM.UI.Components.Overlay, Object, { MochiKit.Base.bind(aFunctionToShowResult, this)(); this.setMessage(aMessage); - MochiKit.Async.callLater(delay, MochiKit.Base.bind(this.hide, this)) + return MochiKit.Async.callLater(delay, MochiKit.Base.bind(this.hide, this)) }, 'hide': function () { var element = this.element(); MochiKit.DOM.removeElementClass(element, 'ios-overlay-show'); MochiKit.DOM.addElementClass(element, 'ios-overlay-hide'); - MochiKit.Async.callLater(1, MochiKit.Style.hideElement, element); + return MochiKit.Async.callLater(1, MochiKit.Style.hideElement, element); }, 'hideSpinner': function () { 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 ad88c98..9e9bd7b 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/Panels/ExtraFeaturesPanel.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/Panels/ExtraFeaturesPanel.js @@ -49,7 +49,9 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({ 'subscription': false, 'data': false, }, - 'isFullyOpen': false + 'isFullyOpen': false, + 'extraFeatureComponentName': null, + 'extraFeatureContent': null }; }, @@ -68,13 +70,9 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({ //========================================================================= -// showDevicePin: function () { -// this.showExtraFeatureContent(Clipperz.PM.UI.Components.ExtraFeatures.DevicePIN()); -// }, - showExtraFeatureComponent: function (aComponentName) { return MochiKit.Base.bind(function () { - this.showExtraFeatureContent(Clipperz.PM.UI.Components.ExtraFeatures[aComponentName]); + this.showExtraFeatureContent(Clipperz.PM.UI.Components.ExtraFeatures[aComponentName], aComponentName); }, this); }, @@ -85,20 +83,21 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({ //------------------------------------------------------------------------- hideExtraFeatureContent: function () { - this.setState({'isFullyOpen':false}); + this.setState({ + 'isFullyOpen': false, + 'extraFeatureComponentName': null, + 'extraFeatureContent': null + }); }, - showExtraFeatureContent: function (aComponent) { + showExtraFeatureContent: function (aComponent, aComponentName) { this.setState({ 'isFullyOpen':true, + 'extraFeatureComponentName': aComponentName, 'extraFeatureContent': aComponent(this.extraFeaturesProps()) }); }, - toggleExtraFeatureContent: function () { - this.setState({'isFullyOpen':!this.state['isFullyOpen']}); - }, - //========================================================================= renderIndex: function () { @@ -119,10 +118,10 @@ 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')}, [ + React.DOM.li({'key':'account_1', 'onClick':this.showExtraFeatureComponent('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'}, "") + React.DOM.p({'key':'account_1_p'}, "Change your account passphrase.") ]) ]), React.DOM.li({'key':'account_2'}, [ @@ -143,10 +142,10 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({ React.DOM.p({}, "") ]) ]), - React.DOM.li({'key':'account_5', 'onClick':this.showExtraFeatureComponent('DeleteAccount')}, [ + React.DOM.li({'key':'account_5', 'onClick':this.showExtraFeatureComponent('DeleteAccount'), 'className':(this.state['extraFeatureComponentName'] == 'DeleteAccount') ? 'selected' : ''}, [ React.DOM.h2({}, "Delete account"), React.DOM.div({}, [ - React.DOM.p({}, "") + React.DOM.p({}, "Delete your account for good.") ]) ]) ]) diff --git a/frontend/delta/js/Clipperz/PM/UI/MainController.js b/frontend/delta/js/Clipperz/PM/UI/MainController.js index f85a2ea..255a149 100644 --- a/frontend/delta/js/Clipperz/PM/UI/MainController.js +++ b/frontend/delta/js/Clipperz/PM/UI/MainController.js @@ -254,10 +254,10 @@ console.log("THE BROWSER IS OFFLINE"); //------------------------------------------------------------------------- - checkPassphrase: function( passphraseIn ) { + checkPassphrase: function (passphraseIn) { var deferredResult; - deferredResult = new Clipperz.Async.Deferred("MainController.deleteAccount_handler", {trace: false}); + deferredResult = new Clipperz.Async.Deferred("MainController.checkPassphrase", {trace: false}); deferredResult.addMethod(this.user(), 'getPassphrase'); deferredResult.addCallback(function (candidatePassphrase, realPassphrase) { return candidatePassphrase == realPassphrase; }, passphraseIn ); @@ -1241,7 +1241,7 @@ console.log("THE BROWSER IS OFFLINE"); var deferredResult; var getPassphraseDelegate; var user; - + getPassphraseDelegate = MochiKit.Base.partial(MochiKit.Async.succeed, newPassphrase); user = new Clipperz.PM.DataModel.User({'username':this.user().username(), 'getPassphraseFunction':getPassphraseDelegate}); @@ -1261,11 +1261,20 @@ console.log("THE BROWSER IS OFFLINE"); deleteAccount_handler: function() { var deferredResult; - + var doneMessageDelay = 2; + deferredResult = new Clipperz.Async.Deferred("MainController.deleteAccount_handler", {trace: false}); + deferredResult.addCallback(MochiKit.Base.method(this, 'ask', { + 'question': "Do you really want to permanently delete your account?", + 'possibleAnswers':{ + 'cancel': {'label':"No", 'isDefault':true, 'answer':MochiKit.Base.methodcaller('cancel', new MochiKit.Async.CancelledError())}, + 'revert': {'label':"Yes", 'isDefault':false, 'answer':MochiKit.Base.methodcaller('callback')} + } + })), deferredResult.addMethod(this.overlay(), 'show', "deleting …", true); deferredResult.addMethod(this.user(), 'deleteAccount'); - deferredResult.addCallback(function() { window.location.href = '/'; }); + deferredResult.addMethod(this.overlay(), 'done', "deleted", doneMessageDelay); + deferredResult.addCallback(MochiKit.Async.callLater, doneMessageDelay, function() { window.location.href = '/'; }); deferredResult.callback(); diff --git a/frontend/delta/scss/core/layout.scss b/frontend/delta/scss/core/layout.scss index a648b8f..ce969d3 100644 --- a/frontend/delta/scss/core/layout.scss +++ b/frontend/delta/scss/core/layout.scss @@ -529,7 +529,7 @@ div.dialogBox { height: 100%; .mask { - z-index: 12; + z-index: 25; } div.dialog { @include flex(none); diff --git a/frontend/delta/scss/style/dialogBox.scss b/frontend/delta/scss/style/dialogBox.scss index 84f4cf7..140d3f4 100644 --- a/frontend/delta/scss/style/dialogBox.scss +++ b/frontend/delta/scss/style/dialogBox.scss @@ -25,13 +25,17 @@ div.dialog { @include box-shadow(0px, 2px, 5px, rgba(50, 50, 50, 0.75)); @include border-radius(8px); + max-width: 70%; background-color: white; padding: 30px; + box-shadow: 4px 4px 6px 5px rgba(0,0,0, 0.3); h3.message { font-size: 18pt; font-weight: bold; padding-bottom: 20px; + white-space: pre-wrap; + word-wrap: break-word; } div.answers { diff --git a/frontend/delta/scss/style/settingsPanel.scss b/frontend/delta/scss/style/settingsPanel.scss index ccbe5f3..a5126e9 100644 --- a/frontend/delta/scss/style/settingsPanel.scss +++ b/frontend/delta/scss/style/settingsPanel.scss @@ -49,8 +49,14 @@ refer to http://www.clipperz.com. // padding-right: 0px; border-bottom: 1px solid white; + & > h1 { + cursor: pointer; + font-size: 16pt; + padding: 10px; + } + & > ul { - padding-left: 10px; +// padding-left: 10px; } // &:last-child { @@ -68,8 +74,15 @@ refer to http://www.clipperz.com. li { & > ul > li { padding: 10px; + padding-left: 20px; padding-right: 0px; + cursor: pointer; + &.selected { + background-color: #333; +// color: black; + } + & > div { padding: 4px; } @@ -83,13 +96,14 @@ refer to http://www.clipperz.com. } - h1 { - cursor: pointer; - font-size: 16pt; - padding: 10px; - } +// h1 { +// cursor: pointer; +// font-size: 16pt; +// padding: 10px; +// } h2 { +// cursor: pointer; font-weight: 300; font-size: 14pt; } @@ -157,6 +171,17 @@ refer to http://www.clipperz.com. display: none; } + .extraFeature { + padding: 20px; + + h1 { + font-size: 20pt; + padding-bottom: 20px; + } + } + + +/* .changePassphraseForm { label { display: block; @@ -184,28 +209,79 @@ refer to http://www.clipperz.com. display: inline-block; } } +*/ form { - input.valid + .invalidMsg, input.empty + .invalidMsg, input:focus + .invalidMsg, input.invalid:focus + .invalidMsg { - visibility: hidden; + + label { + display: none; + } + + input { + $border-size: 0px; // 2px; + + display: block; + font-size: 18pt; + margin-bottom: 8px; + padding: (6px - $border-size) (10px - $border-size); + border: $border-size solid white; + width: 350px; + color: black; + + &.invalid { + border: $border-size solid $clipperz-orange; + color: gray; + } } - input:focus { - border: 2px solid $clipperz-orange; + p { + @include flexbox; + @include flex-direction(row); + + input { + width: 30px; + @include flex(auto); + } + + span { + @include flex(auto); + font-size: 12pt; + } } - - input.valid:focus { - border: 2px solid $clipperz-blue; - } - - input.invalid + .invalidMsg { - visibility: visible; - } - - .invalidMsg::before { - font-family: serif; - content: "\26A0 \0000a0"; + + button { + font-family: "clipperz-font"; +// min-height: 48px; +// min-width: 48px; + + color: white; +// font-size: 24pt; + font-size: 14pt; +// font-weight: 500; + border: 0px; + + margin-top: 20px; + padding: 6px 10px; + + border: 1px solid white; + background-color: $main-color; + @include transition(background-color font-weight, 0.2s, linear); + + &:hover { + }; + + &:disabled { + font-weight: 100; + background-color: #c0c0c0; + cursor: default; + + &:hover { + }; + } } +// input.valid:focus { +// border: 2px solid $clipperz-blue; +// } } } From 574851565555e9383dd3d30428156a6a7920c37b Mon Sep 17 00:00:00 2001 From: Giulio Cesare Solaroli Date: Tue, 14 Apr 2015 17:26:35 +0200 Subject: [PATCH 2/6] Fixed React version in license notes --- frontend/delta/properties/delta.properties.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/delta/properties/delta.properties.json b/frontend/delta/properties/delta.properties.json index 6eb1742..efc063d 100644 --- a/frontend/delta/properties/delta.properties.json +++ b/frontend/delta/properties/delta.properties.json @@ -5,8 +5,8 @@ "mochikit.commit": "6f26f745d5d915540aa0fc6c34fda24952891a9d", "reactjs.repository": "https://github.com/facebook/react.git", - "reactjs.version": "0.11.0", - "reactjs.commit": "95d82cacd6e9cc6a2fe6366d79510cc9133886cb", + "reactjs.version": "0.13.1", + "reactjs.commit": "f308c03455f07ec1c7f422220b411ff1c3b024de", "modernizr.repository": "https://github.com/Modernizr/Modernizr", "modernizr.version": "2.8.2", From fab1d051274925d72abef174ddaae8fb7159e2e3 Mon Sep 17 00:00:00 2001 From: jokajak Date: Wed, 8 Apr 2015 17:12:47 -0400 Subject: [PATCH 3/6] Introduce new flask based python backend This supports most functionality. Tested the following functionality: * Create account * Delete account * Create a card * Download offline copy (couldn't log in) - needs work * Change passphrase * One time password creation and use --- .../flask/properties/flask.properties.json | 5 + backend/flask/src/README.md | 7 + backend/flask/src/clipperz.py | 9 + backend/flask/src/clipperz/__init__.py | 38 ++ backend/flask/src/clipperz/api.py | 622 ++++++++++++++++++ backend/flask/src/clipperz/exceptions.py | 25 + backend/flask/src/clipperz/models.py | 151 +++++ backend/flask/src/clipperz/views.py | 128 ++++ backend/flask/src/config.py | 35 + backend/flask/src/db_create.py | 13 + backend/flask/src/db_downgrade.py | 8 + backend/flask/src/db_migrate.py | 20 + backend/flask/src/db_repository/README | 4 + backend/flask/src/db_repository/__init__.py | 0 backend/flask/src/db_repository/manage.py | 5 + backend/flask/src/db_repository/migrate.cfg | 25 + .../src/db_repository/versions/__init__.py | 0 backend/flask/src/db_upgrade.py | 7 + backend/flask/src/run.sh | 7 + backend/flask/src/setup.py | 72 ++ scripts/builder/backends/flaskBuilder.py | 18 + scripts/builder/frontends/deltaBuilder.py | 3 +- 22 files changed, 1201 insertions(+), 1 deletion(-) create mode 100644 backend/flask/properties/flask.properties.json create mode 100644 backend/flask/src/README.md create mode 100644 backend/flask/src/clipperz.py create mode 100644 backend/flask/src/clipperz/__init__.py create mode 100644 backend/flask/src/clipperz/api.py create mode 100644 backend/flask/src/clipperz/exceptions.py create mode 100644 backend/flask/src/clipperz/models.py create mode 100644 backend/flask/src/clipperz/views.py create mode 100644 backend/flask/src/config.py create mode 100644 backend/flask/src/db_create.py create mode 100644 backend/flask/src/db_downgrade.py create mode 100644 backend/flask/src/db_migrate.py create mode 100644 backend/flask/src/db_repository/README create mode 100644 backend/flask/src/db_repository/__init__.py create mode 100644 backend/flask/src/db_repository/manage.py create mode 100644 backend/flask/src/db_repository/migrate.cfg create mode 100644 backend/flask/src/db_repository/versions/__init__.py create mode 100644 backend/flask/src/db_upgrade.py create mode 100644 backend/flask/src/run.sh create mode 100644 backend/flask/src/setup.py create mode 100755 scripts/builder/backends/flaskBuilder.py diff --git a/backend/flask/properties/flask.properties.json b/backend/flask/properties/flask.properties.json new file mode 100644 index 0000000..6e44ebd --- /dev/null +++ b/backend/flask/properties/flask.properties.json @@ -0,0 +1,5 @@ +{ + "request.path": "../pm", + "dump.path": "/dump", + "should.pay.toll": "false" +} diff --git a/backend/flask/src/README.md b/backend/flask/src/README.md new file mode 100644 index 0000000..6555e72 --- /dev/null +++ b/backend/flask/src/README.md @@ -0,0 +1,7 @@ +clipperz +======== +A flask based backend for the Clipperz (https://clipperz.is) Password Manager. This backend is for development and personal use only. As such it does not implement any bot protection mechanisms such as tolls. + +Running +------- +Once you have built the backend using the clipperz build process you can use run.sh to create an environment for testing against. The database will be created in the target directory which means it will be over-ridden every time you build. To change this you can specify a `DATABASE_URL` environment variable that points to another location. diff --git a/backend/flask/src/clipperz.py b/backend/flask/src/clipperz.py new file mode 100644 index 0000000..6956950 --- /dev/null +++ b/backend/flask/src/clipperz.py @@ -0,0 +1,9 @@ +from clipperz import app, db + + +def main(): + db.create_all() + app.run(debug=True) + +if __name__ == "__main__": + main() diff --git a/backend/flask/src/clipperz/__init__.py b/backend/flask/src/clipperz/__init__.py new file mode 100644 index 0000000..8a6665c --- /dev/null +++ b/backend/flask/src/clipperz/__init__.py @@ -0,0 +1,38 @@ +import os + +from flask import Flask +from flask.ext.login import LoginManager +from flask.ext.sqlalchemy import SQLAlchemy +from simplekv.db.sql import SQLAlchemyStore +from flask.ext.kvsession import KVSessionExtension +from config import * + +APP_ROOT = os.path.dirname(os.path.abspath(__file__)) +app = Flask(__name__, static_url_path='') +lm = LoginManager() +lm.init_app(app) +app.config.from_object(DevelopmentConfig) +db = SQLAlchemy(app) +store = SQLAlchemyStore(db.engine, db.metadata, 'sessions') +kvsession = KVSessionExtension(store, app) + +if not app.debug and os.environ.get('HEROKU') is None: + import logging + from logging.handlers import RotatingFileHandler + file_handler = RotatingFileHandler('tmp/microblog.log', 'a', + 1 * 1024 * 1024, 10) + file_handler.setLevel(logging.INFO) + file_handler.setFormatter(logging.Formatter( + '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]')) + app.logger.addHandler(file_handler) + app.logger.setLevel(logging.INFO) + app.logger.info('microblog startup') + +if os.environ.get('HEROKU') is not None: + import logging + stream_handler = logging.StreamHandler() + app.logger.addHandler(stream_handler) + app.logger.setLevel(logging.INFO) + app.logger.info('microblog startup') + +from clipperz import views, models, api diff --git a/backend/flask/src/clipperz/api.py b/backend/flask/src/clipperz/api.py new file mode 100644 index 0000000..26d67d0 --- /dev/null +++ b/backend/flask/src/clipperz/api.py @@ -0,0 +1,622 @@ +import json +import random +import hashlib + +from flask import jsonify, session, g +from datetime import datetime +from flask.ext.login import logout_user, current_user, login_user, \ + login_required +from sqlalchemy.orm.exc import NoResultFound +from clipperz import app, db +from .exceptions import InvalidUsage +from .models import User, Record, RecordVersion, OneTimePassword + + +#============================================================================== +# Helpers +#============================================================================== +def randomSeed(): + return hex(random.getrandbits(32*8))[2:-1] + + +def clipperzHash(aString): + firstRound = hashlib.sha256() + firstRound.update(aString) + result = hashlib.sha256() + result.update(firstRound.digest()) + + return result.hexdigest() +#============================================================================== +# Method handlers +#============================================================================== + + +class HandlerMixin: + def handle_request(self, request): + parameters = json.loads(request.form['parameters']) + app.logger.debug('raw parameters: %s', parameters) + parameters = parameters['parameters'] + if 'message' in parameters: + message = parameters['message'] + app.logger.debug('message: %s', message) + app.logger.debug('parameters: %s', parameters) + try: + handler = getattr(self, message) + except AttributeError: + raise InvalidUsage( + 'This message handler is not yet implemented for this method', + status_code=501) + return handler(parameters, request) + + +class registration(HandlerMixin): + def completeRegistration(self, parameters, request): + credentials = parameters['credentials'] + data = parameters['user'] + user = User() + user.updateCredentials(credentials) + user.update(data) + db.session.add(user) + db.session.commit() + return jsonify(lock=user.lock, + result='done') + + +class handshake(HandlerMixin): + srp_n = '115b8b692e0e045692cf280b436735c77a5a9e8a9e7ed56c965f87db5b2a2ece3' + srp_g = 2 + srp_n = long(srp_n, 16) + + def connect(self, parameters, request): + result = {} + session['C'] = parameters['parameters']['C'] + session['A'] = parameters['parameters']['A'] + app.logger.debug('username: %s', session['C']) + + user = User().query.filter_by(username=session['C']).one() + + if user is not None and session['A'] != 0: + session['s'] = user.srp_s + session['v'] = user.srp_v + if 'otpid' in session: + try: + otpId = session['otpId'] + + one_time_password = OneTimePassword().filter_by( + id=otpId + ).one() + + if one_time_password.user.username != user.username: + one_time_password.reset('DISABLED') + raise Exception(("user mismatch between the current " + "session and 'one time password' " + "user")) + elif one_time_password.status != 'requested': + one_time_password.reset('DISABLED') + raise Exception(("Trying to use an 'one time password'" + " in the wrong state")) + + one_time_password.reset("USED") + + result['oneTimePassword'] = one_time_password.reference + db.session.add(one_time_password) + db.session.commit() + + except Exception, detail: + app.logger.error("connect.optid: " + str(detail)) + + else: + # invalid user + invalid = ('112233445566778899aabbccddeeff00112233445566778899' + 'aabbccddeeff00') + session['s'] = invalid + session['v'] = invalid + + session['b'] = randomSeed() + k = '0x64398bff522814e306a97cb9bfc4364b7eed16a8c17c5208a40a2bad2933c8e' + k = long(k, 16) + app.logger.debug('k: %s (%s)', k, hex(k)) + session['B'] = hex(k * long("0x%s" % session['v'], 16) + + pow(self.srp_g, + long("0x%s" % session['b'], 16), + self.srp_n) + )[2:-1] + result['s'] = session['s'] + result['B'] = session['B'] + app.logger.debug('Session: %s', session) + return jsonify({'result': result}) + + def credentialCheck(self, parameters, request): + country = 'US' + # hard-coded for development + result = { + 'accountInfo': { + 'features': [ + 'UPDATE_CREDENTIALS', + 'EDIT_CARD', + 'CARD_DETAILS', + 'ADD_CARD', + 'DELETE_CARD', + 'OFFLINE_COPY', + 'LIST_CARDS' + ], + 'paramentVerificationPending': False, + 'currentSubscriptionType': 'EARLY_ADOPTER', + 'isExpiring': False, + 'latestActiveLevel': 'EARLY_ADOPTER', + 'payments': [], + 'featureSet': 'FULL', + 'latestActiveThreshold': -1.0, + 'referenceDate': datetime.now(), + 'isExpired': False, + 'expirationDate': datetime(4001, 1, 1) + }, + } + + A = long("0x%s" % session['A'], 16) + B = long("0x%s" % session['B'], 16) + b = long("0x%s" % session['b'], 16) + v = long("0x%s" % session['v'], 16) + u = long("0x%s" % clipperzHash(str(A) + str(B)), 16) + s = long("0x%s" % session['s'], 16) + C = session['C'] + n = self.srp_n + + S = pow((A * pow(v, u, n)), b, n) + K = clipperzHash(str(S)) + M1 = '{0}{1}{2}{3}{4}{5}{6}'.format( + '5976268709782868014401975621485889074340014836557888656093758064', + '39877501869636875571920406529', + clipperzHash(str(C)), + str(s), + str(A), + str(B), + str(K) + ) + M1 = clipperzHash(M1) + if M1 == parameters['parameters']['M1']: + session['K'] = K + M2 = clipperzHash(str(A) + M1 + K) + + result['M2'] = M2 + result['connectionId'] = '' + result['loginInfo'] = {} + result['loginInfo']['current'] = { + 'date': datetime.now(), + 'ip': request.remote_addr, + 'browser': request.user_agent.browser, + 'operatingSystem': request.user_agent.platform, + 'disconnectionType': 'STILL_CONNECTED', + 'country': country + }, + result['loginInfo']['latest'] = {} + result['offlineCopyNeeded'] = False + user = User().query.filter_by(username=session['C']).one() + result['lock'] = user.lock + login_user(user) + session['User'] = user + else: + result['error'] = '?' + + result['s'] = session['s'] + result['B'] = session['B'] + return jsonify({'result': result}) + + def oneTimePassword(self, parameters, request): + #"parameters": { + #"message": "oneTimePassword", + #"version": "0.2", + #"parameters": { + # "oneTimePasswordKey": "03bd882...396082c", + # "oneTimePasswordKeyChecksum": "f73f629...041031d" + #} + #} + result = {} + + try: + key = parameters['parameters']['oneTimePasswordKey'] + checksum = parameters['parameters']['oneTimePasswordKeyChecksum'] + otp = OneTimePassword().query.filter_by(key_value=key).one() + if otp.status == 'ACTIVE': + if otp.key_checksum == checksum: + session['userId'] = otp.user.id + session['otpId'] = otp.id + result['data'] = otp.data + result['version'] = otp.version + otp.data = '' + otp.status = 'REQUESTED' + else: + otp.data = '' + otp.status = 'DISABLED' + db.session.add(otp) + db.session.commit() + except NoResultFound, details: + app.logger.debug('OTP No Results Found: ', details) + + return jsonify({'result': result}) + + +class message(HandlerMixin): + @login_required + def getUserDetails(self, parameters, request): + app.logger.debug(parameters) + if 'srpSharedSecret' not in parameters: + raise InvalidUsage( + 'Mal-formed message format.', + status_code=400) + srpSharedSecret = parameters['srpSharedSecret'] + if srpSharedSecret != session['K']: + raise InvalidUsage( + 'Your session is invalid, please re-authenticate', + status_code=401) + # Online results + # {"result": + # { + # "header": "{\"records\":{\"index\":{\"383036...eeefbe48\":\"0\"},\"data\":\"zrhb3/791SDdb48v3vXfPzeDrv0Jhs4rAaOKHx+jDF6pwm/qi9DGSR0JwrprOgwv3bjYJgU2xHA8cuA0bPvABHSHK6fnGwvhSlyYjskY2Cy/WbRJhcA4kw+VUsOjZPRxtM8bSJnSxViAXsghTcya6+5M3MdMJHE=\"},\"directLogins\":{\"index\":{},\"data\":\"s7KYzHwKISmjYufv9h0mpTiM\"},\"preferences\":{\"data\":\"mf8fWjpOQjlV18ukEO9FN3LP\"},\"oneTimePasswords\":{\"data\":\"8tV1yRHv30lsl3FadG9YnTOo\"},\"version\":\"0.1\"}", + # "lock": "3D4B4501-D7A9-6E4F-A487-9428C0B6E79D", + # "version": "0.4", + # "recordsStats": { + # "383036...eeefbe48":{ + # "updateDate": "Sun, 12 April 2015 17:11:01 UTC", + # "accessDate": "Sun, 12 April 2015 17:11:01 UTC" + # } + # }, + # "offlineCopyNeeded":true, + # "statistics":"ByYItDeZMdZ+e/pafp14bGrR" + # } + # } + # Dev results + #{"result": + # {"header": "{\"records\":{\"index\":{\"843a95d8...5f734b\":\"1\"},\"data\":\"fKgc5Jt9JH/CibCIpcRmwyLuLIvufWchNJga7GoFcWT9K8LR+ai0BvzWBUxcPccivE9zPv2Swe5E8wPEIc+Lv0U73NobJEct7WqBcCdLxszBE1SokxPEZDUVdWVQtAiwgOS219inCFmI5CaB\"},\"directLogins\":{\"index\":{},\"data\":\"rnMQBB81ezh6JKNGXkDCyY+q\"},\"preferences\":{\"data\":\"9jzR9Goo5PGpXbAdmsXHuQGp\"},\"oneTimePasswords\":{\"data\":\"iXEUuQGskZhMyHEwU+3tRGQM\"},\"version\":\"0.1\"}", + # "recordStats": { + # "843a95d8...5f734b": { + # "updateDate": "Sun, 12 Apr 2015 13:08:44 GMT" + # } + # }, + # "statistics": "", + # "version": "0.4"}} + result = {} + user = User().query.filter_by(username=session['C']).one() + + records_stats = {} + for record in user.records: + records_stats[record.reference] = { + 'updateDate': record.update_date, + 'accessDate': record.access_date + } + + result['recordsStats'] = records_stats + result['header'] = user.header + result['statistics'] = user.statistics + result['version'] = user.version + result['offlineCopyNeeded'] = not user.offline_saved + result['lock'] = user.lock + return jsonify({'result': result}) + + @login_required + def saveChanges(self, parameters, request): + result = {} + parameters = parameters['parameters'] + if ('user' not in parameters + or 'records' not in parameters): + app.logger.debug('saveChanges parameters: %s', parameters) + raise InvalidUsage( + 'Mal-formed message format.', + status_code=400) + + user_data = parameters['user'] + record_data = parameters['records'] + + user = User().query.filter_by(username=session['C']).one() + app.logger.debug('user_data: %s', user_data) + user.update(user_data) + db.session.add(user) + + if 'updated' in record_data: + for entry in record_data['updated']: + reference = entry['record']['reference'] + try: + record = Record().query.filter_by(reference=reference).one() + except NoResultFound: + record = Record(user=user) + record_version = RecordVersion(record=record) + record_version.update(entry) + db.session.add(record) + db.session.add(record_version) + + if 'deleted' in record_data: + for reference in record_data['deleted']: + try: + record = Record().query.filter_by(reference=reference).one() + db.session.delete(record) + except NoResultFound: + pass + + db.session.commit() + result['lock'] = user.lock + result['result'] = 'done' + + return jsonify({'result': result}) + + @login_required + def getRecordDetail(self, parameters, request): + #{ + # "parameters": { + # "srpSharedSecret": "bf79ad3cf0c1...63462a9fb560", + # "message": "getRecordDetail", + # "parameters": { + # "reference": "e3a5856...20e080fc97f13c14c" + # } + # } + #} + app.logger.debug(parameters) + if 'srpSharedSecret' not in parameters: + raise InvalidUsage( + 'Mal-formed message format.', + status_code=400) + srpSharedSecret = parameters['srpSharedSecret'] + if (srpSharedSecret != session['K'] and session['User'] != g.user): + raise InvalidUsage( + 'Your session is incorrect, please re-authenticate', + status_code=401) + + reference = parameters['parameters']['reference'] + result = {} + + record = Record().query.filter_by(reference=reference).one() + app.logger.debug(record.current_record_version) + record_versions = {} + oldest_encryption_version = None + versions = RecordVersion().query.filter_by(record=record).all() + for record_version in versions: + version_entry = {} + version_entry['reference'] = record_version.reference + version_entry['data'] = record_version.data + version_entry['header'] = record_version.header + version_entry['version'] = record_version.api_version + version_entry['creationDate'] = record_version.creation_date + version_entry['updateDate'] = record_version.update_date + version_entry['accessDate'] = record_version.access_date + try: + previous_version = RecordVersion().query.filter_by( + id=record_version.previous_version_id).one() + reference = previous_version.reference + key = record_version.previous_version_key + version_entry['previousVersion'] = reference + version_entry['previousVersionKey'] = key + except NoResultFound: + pass + if (not oldest_encryption_version + or oldest_encryption_version > record_version.api_version): + oldest_encryption_version = record_version.api_version + record_versions[record_version.reference] = version_entry + + result['reference'] = record.reference + result['data'] = record.data + result['version'] = record.api_version + result['creationDate'] = str(record.creation_date) + result['updateDate'] = str(record.update_date) + result['accessDate'] = str(record.access_date) + result['oldestUsedEncryptedVersion'] = oldest_encryption_version + result['versions'] = record_versions + result['currentVersion'] = record.current_record_version.reference + return jsonify({'result': result}) + + @login_required + def getOneTimePasswordsDetails(self, parameters, request): + #{ + # "parameters": { + # "srpSharedSecret": "bf79ad3cf0c1...63462a9fb560", + # "message": "getOneTimePasswordsDetails", + # "parameters": {} + # } + #} + if 'srpSharedSecret' not in parameters: + raise InvalidUsage( + 'Mal-formed message format.', + status_code=400) + srpSharedSecret = parameters['srpSharedSecret'] + if (srpSharedSecret != session['K'] and session['User'] != g.user): + raise InvalidUsage( + 'Your session is incorrect, please re-authenticate', + status_code=401) + + result = {} + + otps = OneTimePassword().query.filter_by(user=current_user).all() + for otp in otps: + #{"e8541...af0c6b951":{"status":"ACTIVE"}} + result[otp.reference] = {'status': otp.status} + + return jsonify({'result': result}) + + @login_required + def getLoginHistory(self, parameters, request): + #{ + # "parameters": { + # "srpSharedSecret": "bf79ad3cf0c1...63462a9fb560", + # "message": "getOneTimePasswordsDetails", + # "parameters": {} + # } + #} + if 'srpSharedSecret' not in parameters: + raise InvalidUsage( + 'Mal-formed message format.', + status_code=400) + srpSharedSecret = parameters['srpSharedSecret'] + if (srpSharedSecret != session['K'] and session['User'] != g.user): + raise InvalidUsage( + 'Your session is incorrect, please re-authenticate', + status_code=401) + + result = {} + + user = User().query.filter_by(username=session['C']).one() + + if (user != g.user): + raise InvalidUsage( + 'Your session is incorrect, please re-authenticate', + status_code=401) + + return jsonify({'result': result}) + + @login_required + def addNewOneTimePassword(self, parameters, request): + # "parameters": { + # "message": "addNewOneTimePassword", + # "srpSharedSecret": "1e8e037a8...85680f931d45dfc20472cf9d1", + # "parameters": { + # "user": { + # "header":
+ # "statistics": "WxHa6VSMmZunOjLCwAVQrkYI", + # "version": "0.4", + # "lock": "new lock" + # }, + # "oneTimePassword": { + # "reference": "ffaec6f...7b123d39b8965e7e5", + # "key": "496dc431db...faec137698b16c", + # "keyChecksum": "f927c1...eb970552360a311dda", + # "data": "GcfCFsoSc5RT...MF8nstFXXHYSXF+Vyj4w=", + # "version": "0.4" + # } + # } + # } + #} + if 'srpSharedSecret' not in parameters: + raise InvalidUsage( + 'Mal-formed message format.', + status_code=400) + srpSharedSecret = parameters['srpSharedSecret'] + if (srpSharedSecret != session['K'] and session['User'] != g.user): + raise InvalidUsage( + 'Your session is incorrect, please re-authenticate', + status_code=401) + + result = {} + parameters = parameters['parameters'] + + user = User().query.filter_by(username=session['C']).one() + + if (user != g.user): + raise InvalidUsage( + 'Your session is incorrect, please re-authenticate', + status_code=401) + + user_data = parameters['user'] + + app.logger.debug('user_data: %s', user_data) + user.update(user_data) + db.session.add(user) + + one_time_password = parameters['oneTimePassword'] + otp = OneTimePassword( + reference=one_time_password['reference'], + key_value=one_time_password['key'], + key_checksum=one_time_password['keyChecksum'], + data=one_time_password['data'], + version=one_time_password['version'], + user=user, + status='ACTIVE' + ) + db.session.add(otp) + db.session.commit() + + return jsonify({'result': result}) + + def echo(self, parameters, request): + result = {} + if 'srpSharedSecret' not in parameters: + raise InvalidUsage( + 'Mal-formed message format.', + status_code=400) + srpSharedSecret = parameters['srpSharedSecret'] + if (srpSharedSecret != session['K'] and session['User'] != g.user): + raise InvalidUsage( + 'Your session is incorrect, please re-authenticate', + status_code=401) + + user = User().query.filter_by(username=session['C']).one() + + if (user != g.user): + raise InvalidUsage( + 'Your session is incorrect, please re-authenticate', + status_code=401) + + user.offline_saved = True + db.session.add(user) + db.session.commit() + return jsonify({'result': result}) + + @login_required + def deleteUser(self, parameters, request): + result = {} + if 'srpSharedSecret' not in parameters: + raise InvalidUsage( + 'Mal-formed message format.', + status_code=400) + srpSharedSecret = parameters['srpSharedSecret'] + if (srpSharedSecret != session['K'] and session['User'] != g.user): + raise InvalidUsage( + 'Your session is incorrect, please re-authenticate', + status_code=401) + + user = User().query.filter_by(username=session['C']).one() + + if (user != g.user): + raise InvalidUsage( + 'Your session is incorrect, please re-authenticate', + status_code=401) + + db.session.delete(user) + db.session.commit() + return jsonify({'result': result}) + + @login_required + def upgradeUserCredentials(self, parameters, request): + #{"parameters":{"message":"upgradeUserCredentials","srpSharedSecret":"36...d6","parameters":{"credentials":{"C":"59d02038fdb47cee5b7837a697bc8ff41cc66d8844c8fce844cdf45b0b08b1e4","s":"fe40513b99fbaca9bfe51b8d6e9b3eb42b1e01ce8b0ae32461bec0294c1030ed","v":"300b92f4a3e34034d78cd5081f8db36dbf2a4c5f7a41db6954518815a3554278","version":"0.2"},"user":{"header":"{\"records\":{\"index\":{},\"data\":\"VIIDc5vFNoIflyXF8syb8fRS\"},\"directLogins\":{\"index\":{},\"data\":\"9elg3tu2UqsJ0zbUAdQkLE69\"},\"preferences\":{\"data\":\"Sbwar35Ynd/XobuAm4K66lqj\"},\"oneTimePasswords\":{\"data\":\"tAcTsWVTwALSfxXvCChHi4FD\"},\"version\":\"0.1\"}","statistics":"","version":"0.4","lock":null}}}} + result = {} + if 'srpSharedSecret' not in parameters: + raise InvalidUsage( + 'Mal-formed message format.', + status_code=400) + srpSharedSecret = parameters['srpSharedSecret'] + if (srpSharedSecret != session['K'] and session['User'] != g.user): + raise InvalidUsage( + 'Your session is incorrect, please re-authenticate', + status_code=401) + + user = User().query.filter_by(username=session['C']).one() + + if (user != g.user): + raise InvalidUsage( + 'Your session is incorrect, please re-authenticate', + status_code=401) + + parameters = parameters['parameters'] + user.updateCredentials(parameters['credentials']) + user.update(parameters['user']) + + if 'oneTimePasswords' in parameters: + for otpRef in parameters['oneTimePasswords']: + try: + otp = OneTimePassword().query.filter_by( + reference=otpRef).one() + otp.data = parameters['oneTimePasswords'][otpRef] + db.session.add(otp) + except NoResultFound: + pass + + db.session.add(user) + db.session.commit() + result['lock'] = user.lock + result['result'] = 'done' + return jsonify({'result': result}) + + +class logout(HandlerMixin): + def handle_request(self, request): + result = {} + logout_user() + session.clear() + result['method'] = 'logout' + return jsonify({'result': result}) diff --git a/backend/flask/src/clipperz/exceptions.py b/backend/flask/src/clipperz/exceptions.py new file mode 100644 index 0000000..5ada520 --- /dev/null +++ b/backend/flask/src/clipperz/exceptions.py @@ -0,0 +1,25 @@ +from clipperz import app +from flask import jsonify + + +class InvalidUsage(Exception): + status_code = 400 + + def __init__(self, message, status_code=None, payload=None): + Exception.__init__(self) + self.message = message + if status_code is not None: + self.status_code = status_code + self.payload = payload + + def to_dict(self): + rv = dict(self.payload or ()) + rv['message'] = self.message + return rv + + +@app.errorhandler(InvalidUsage) +def handle_invalid_usage(error): + response = jsonify(error.to_dict()) + response.status_code = error.status_code + return response diff --git a/backend/flask/src/clipperz/models.py b/backend/flask/src/clipperz/models.py new file mode 100644 index 0000000..9737f37 --- /dev/null +++ b/backend/flask/src/clipperz/models.py @@ -0,0 +1,151 @@ +import datetime + +from flask.ext.login import UserMixin + +from clipperz import app, db + + +class User(db.Model, UserMixin): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(), unique=True, index=True) + srp_s = db.Column(db.String(128)) + srp_v = db.Column(db.String(128)) + header = db.Column(db.Text()) + statistics = db.Column(db.Text()) + auth_version = db.Column(db.String()) + version = db.Column(db.String()) + lock = db.Column(db.String()) + records = db.relationship( + 'Record', + backref='user', + lazy='dynamic', + cascade='save-update, merge, delete, delete-orphan') + otps = db.relationship( + 'OneTimePassword', + backref='user', + lazy='dynamic', + cascade='save-update, merge, delete, delete-orphan') + offline_saved = db.Column(db.Boolean(), default=False) + update_date = db.Column(db.DateTime(), nullable=True) + + def updateCredentials(self, credentials): + self.username = credentials['C'] + self.srp_s = credentials['s'] + self.srp_v = credentials['v'] + self.auth_version = credentials['version'] + + def update(self, data): + self.header = data['header'] + self.statistics = data['statistics'] + self.version = data['version'] + if 'lock' in data: + self.lock = data['lock'] + self.update_date = datetime.datetime.utcnow() + self.offline_saved = False + + def __repr__(self): + return '' % (self.username) + +#------------------------------------------------------------------------------ + + +class RecordVersion(db.Model): + id = db.Column(db.Integer(), primary_key=True) + reference = db.Column(db.String(), unique=True, index=True) + header = db.Column(db.Text()) + data = db.Column(db.Text()) + api_version = db.Column(db.String()) + version = db.Column(db.Integer()) + previous_version_key = db.Column(db.String()) + previous_version_id = db.Column(db.Integer(), + db.ForeignKey('record_version.id')) + creation_date = db.Column(db.DateTime(), default=datetime.datetime.utcnow) + update_date = db.Column(db.DateTime(), default=datetime.datetime.utcnow) + access_date = db.Column(db.DateTime(), default=datetime.datetime.utcnow) + + record_id = db.Column(db.Integer(), + db.ForeignKey('record.id'), + nullable=False) + + record = db.relationship('Record', + backref=db.backref('record_versions', + order_by=id, + cascade='all,delete')) + + def update(self, someData): + app.logger.debug(someData) + recordVersionData = someData['currentRecordVersion'] + self.reference = recordVersionData['reference'] + self.data = recordVersionData['data'] + self.api_version = recordVersionData['version'] + self.version = self.record.version + self.previous_version_key = recordVersionData['previousVersionKey'] + self.update_date = datetime.datetime.utcnow() + + self.record.update(someData['record'], self) +#------------------------------------------------------------------------------ + + +class Record(db.Model): + id = db.Column(db.Integer(), primary_key=True) + user_id = db.Column(db.ForeignKey('user.id')) + reference = db.Column(db.String(), unique=True, index=True) + data = db.Column(db.Text()) + api_version = db.Column(db.String()) + version = db.Column(db.Integer(), default=0) + creation_date = db.Column(db.DateTime(), default=datetime.datetime.utcnow) + update_date = db.Column(db.DateTime(), default=datetime.datetime.utcnow) + access_date = db.Column(db.DateTime(), default=datetime.datetime.utcnow) + + current_record_version = db.relationship( + 'RecordVersion', + uselist=False, + cascade='save-update, merge, delete, delete-orphan') + + def update(self, data, record_version): + self.reference = data['reference'] + self.data = data['data'] + self.api_version = data['version'] + self.update_date = datetime.datetime.now() + self.current_record_version = record_version + if self.version: + self.version += 1 + else: + self.version = 1 + +#------------------------------------------------------------------------------ + + +class OneTimePassword(db.Model): + id = db.Column(db.Integer(), primary_key=True) + user_id = db.Column(db.ForeignKey('user.id')) + status = db.Column(db.String()) + reference = db.Column(db.String(), unique=True) + key_value = db.Column(db.String()) + key_checksum = db.Column(db.String()) + data = db.Column(db.Text()) + version = db.Column(db.String()) + creation_date = db.Column(db.DateTime(), default=datetime.datetime.utcnow) + request_date = db.Column(db.DateTime()) + usage_date = db.Column(db.DateTime()) + + def update(self, someParameters, aStatus): + self.reference = someParameters['reference'] + self.key_value = someParameters['key'] + self.key_checksum = someParameters['keyChecksum'] + self.data = someParameters['data'] + self.version = someParameters['version'] + self.status = aStatus + + def reset(self, aStatus): + self.data = "" + self.status = aStatus + return self + +#------------------------------------------------------------------------------ + + +class Session(db.Model): + id = db.Column(db.Integer(), primary_key=True) + sessionId = db.Column(db.String()) + access_date = db.Column(db.DateTime(), default=datetime.datetime.utcnow) diff --git a/backend/flask/src/clipperz/views.py b/backend/flask/src/clipperz/views.py new file mode 100644 index 0000000..e70f0ac --- /dev/null +++ b/backend/flask/src/clipperz/views.py @@ -0,0 +1,128 @@ +from flask import session, request, g, jsonify +from clipperz import app, db, lm +from .models import User +from .api import * +from .exceptions import InvalidUsage +from flask.ext.login import login_required + + +@lm.user_loader +def load_user(id): + return User.query.get(int(id)) + + +@app.before_request +def before_request(): + g.user = current_user + + +@app.teardown_appcontext +def shutdown_session(exception=None): + db.session.remove() + + +@app.route('/beta/dump/') +@app.route('/gamma/dump/') +@app.route('/delta/dump/') +@login_required +def dump(frontend_version): + user = User().query.filter_by(username=session['C']).one() + + if (user != g.user): + raise InvalidUsage( + 'Your session is incorrect, please re-authenticate', + status_code=401) + + user.offline_saved = True + db.session.add(user) + db.session.commit() + user_data = {} + user_data['users'] = { + 'catchAllUser': { + '__masterkey_test_value__': 'masterkey', + 's': ('112233445566778899aabbccddeeff00112233445566778899' + 'aabbccddeeff00'), + 'v': ('112233445566778899aabbccddeeff00112233445566778899' + 'aabbccddeeff00'), + } + } + + records = {} + for current_record in user.records: + versions = {} + for version in current_record.record_versions: + versions[version.reference] = { + 'header': version.header, + 'data': version.data, + 'version': version.api_version, + 'creationDate': str(version.creation_date), + 'updateDate': str(version.update_date), + 'accessDate': str(version.access_date) + } + + records[current_record.reference] = { + 'data': current_record.data, + 'version': current_record.version, + 'creationDate': str(current_record.creation_date), + 'updateDate': str(current_record.update_date), + 'accessDate': str(current_record.access_date), + 'currentVersion': current_record.current_record_version, + 'versions': versions + } + + user_data['users'][user.username] = { + 's': user.srp_s, + 'v': user.srp_v, + 'version': user.auth_version, + 'maxNumberOfRecords': '100', + 'userDetails': user.header, + 'statistics': user.statistics, + 'userDetailsVersion': user.version, + 'records': records + } + + offline_data_placeholder = ( + '_clipperz_data_ = {user_data}\n' + 'Clipperz.PM.Proxy.defaultProxy = new Clipperz.PM.Proxy.Offline();' + '\n' + 'Clipperz.Crypto.PRNG.defaultRandomGenerator()' + '.fastEntropyAccumulationForTestingPurpose();' + '\n').format(user_data=user_data) + + with open(os.path.join(APP_ROOT, + '{0}/index.html'.format(frontend_version))) as f: + offline_dump = f.read() + + offline_dump = offline_dump.replace('/*offline_data_placeholder*/', + offline_data_placeholder) + response = make_response(offline_dump) + content_disposition = "attachment; filename='Clipperz.html'" + response.headers['Content-Disposition'] = content_disposition + + return response + + +@app.route('/beta/') +def beta(path): + return send_from_directory('beta', path) + + +@app.route('/gamma/') +def gamma(path): + return send_from_directory('gamma', path) + + +@app.route('/delta/') +def delta(path): + return send_from_directory('delta', path) + + +@app.route('/pm', methods=['GET', 'OPTIONS', 'POST']) +def pm(): + method = request.form['method'] + if method not in globals(): + raise InvalidUsage('This method is not yet implemented', + status_code=501) + handler = globals()[method]() + app.logger.debug(method) + return handler.handle_request(request) diff --git a/backend/flask/src/config.py b/backend/flask/src/config.py new file mode 100644 index 0000000..a32c6f7 --- /dev/null +++ b/backend/flask/src/config.py @@ -0,0 +1,35 @@ +import datetime +import os +basedir = os.path.abspath(os.path.dirname(__file__)) + + +CSRF_ENABLED = True + + +if os.environ.get('DATABASE_URL') is None: + SQLALCHEMY_DATABASE_URI = ('sqlite:///' + os.path.join(basedir, 'app.db') + + '?check_same_thread=False') +else: + SQLALCHEMY_DATABASE_URI = os.environ['DATABASE_URL'] +SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository') +SQLALCHEMY_RECRD_QUERIES = True + +ADMINS = ['you@example.com'] + + +class Config(object): + DEBUG = False + TESTING = False + SQLALCHEMY_ECHO = False + WTF_CSRF_ENABLED = True + SECRET_KEY = 'you-will-never-guess' + sessionTimeout = datetime.timedelta(minutes=-2) + + +class DevelopmentConfig(Config): + DEBUG = True + SQLALCHEMY_ECHO = True + + +class TestingConfig(Config): + TESTING = True diff --git a/backend/flask/src/db_create.py b/backend/flask/src/db_create.py new file mode 100644 index 0000000..cd6ce69 --- /dev/null +++ b/backend/flask/src/db_create.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +from migrate.versioning import api +from config import SQLALCHEMY_DATABASE_URI +from config import SQLALCHEMY_MIGRATE_REPO +from app import db +import os.path +db.create_all() +if not os.path.exists(SQLALCHEMY_MIGRATE_REPO): + api.create(SQLALCHEMY_MIGRATE_REPO, 'database repository') + api.version_control(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) +else: + api.version_control(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO, + api.version(SQLALCHEMY_MIGRATE_REPO)) diff --git a/backend/flask/src/db_downgrade.py b/backend/flask/src/db_downgrade.py new file mode 100644 index 0000000..c001e6c --- /dev/null +++ b/backend/flask/src/db_downgrade.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +from migrate.versioning import api +from config import SQLALCHEMY_DATABASE_URI +from config import SQLALCHEMY_MIGRATE_REPO +v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) +api.downgrade(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO, v - 1) +v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) +print('Current database version: ' + str(v)) diff --git a/backend/flask/src/db_migrate.py b/backend/flask/src/db_migrate.py new file mode 100644 index 0000000..03f65d1 --- /dev/null +++ b/backend/flask/src/db_migrate.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +import imp +from migrate.versioning import api +from app import db +from config import SQLALCHEMY_DATABASE_URI +from config import SQLALCHEMY_MIGRATE_REPO +v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) +migration = SQLALCHEMY_MIGRATE_REPO + ('/versions/%03d_migration.py' % (v+1)) +tmp_module = imp.new_module('old_model') +old_model = api.create_model(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) +exec(old_model, tmp_module.__dict__) +script = api.make_update_script_for_model(SQLALCHEMY_DATABASE_URI, + SQLALCHEMY_MIGRATE_REPO, + tmp_module.meta, + db.metadata) +open(migration, "wt").write(script) +api.upgrade(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) +v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) +print('New migration saved as ' + migration) +print('Current database version: ' + str(v)) diff --git a/backend/flask/src/db_repository/README b/backend/flask/src/db_repository/README new file mode 100644 index 0000000..6218f8c --- /dev/null +++ b/backend/flask/src/db_repository/README @@ -0,0 +1,4 @@ +This is a database migration repository. + +More information at +http://code.google.com/p/sqlalchemy-migrate/ diff --git a/backend/flask/src/db_repository/__init__.py b/backend/flask/src/db_repository/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/flask/src/db_repository/manage.py b/backend/flask/src/db_repository/manage.py new file mode 100644 index 0000000..554f89c --- /dev/null +++ b/backend/flask/src/db_repository/manage.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +from migrate.versioning.shell import main + +if __name__ == '__main__': + main() diff --git a/backend/flask/src/db_repository/migrate.cfg b/backend/flask/src/db_repository/migrate.cfg new file mode 100644 index 0000000..40bb1e3 --- /dev/null +++ b/backend/flask/src/db_repository/migrate.cfg @@ -0,0 +1,25 @@ +[db_settings] +# Used to identify which repository this database is versioned under. +# You can use the name of your project. +repository_id=clpperz + +# The name of the database table used to track the schema version. +# This name shouldn't already be used by your project. +# If this is changed once a database is under version control, you'll need to +# change the table name in each database too. +version_table=migrate_version + +# When committing a change script, Migrate will attempt to generate the +# sql for all supported databases; normally, if one of them fails - probably +# because you don't have that database installed - it is ignored and the +# commit continues, perhaps ending successfully. +# Databases in this list MUST compile successfully during a commit, or the +# entire commit will fail. List the databases your application will actually +# be using to ensure your updates to that database work properly. +# This must be a list; example: ['postgres','sqlite'] +required_dbs=[] + +# When creating new change scripts, Migrate will stamp the new script with +# a version number. By default this is latest_version + 1. You can set this +# to 'true' to tell Migrate to use the UTC timestamp instead. +use_timestamp_numbering=False diff --git a/backend/flask/src/db_repository/versions/__init__.py b/backend/flask/src/db_repository/versions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/flask/src/db_upgrade.py b/backend/flask/src/db_upgrade.py new file mode 100644 index 0000000..f5ae27b --- /dev/null +++ b/backend/flask/src/db_upgrade.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +from migrate.versioning import api +from config import SQLALCHEMY_DATABASE_URI +from config import SQLALCHEMY_MIGRATE_REPO +api.upgrade(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) +v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) +print('Current database version: ' + str(v)) diff --git a/backend/flask/src/run.sh b/backend/flask/src/run.sh new file mode 100644 index 0000000..34f0381 --- /dev/null +++ b/backend/flask/src/run.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env sh +# Sets up flask development environment and activates it + +virtualenv . + +bin/python setup.py develop +bin/python clipperz.py diff --git a/backend/flask/src/setup.py b/backend/flask/src/setup.py new file mode 100644 index 0000000..affb5e5 --- /dev/null +++ b/backend/flask/src/setup.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +from __future__ import print_function +from setuptools import setup +from setuptools.command.test import test as TestCommand +import io +import os +import sys + +here = os.path.abspath(os.path.dirname(__file__)) + + +def read(*filenames, **kwargs): + encoding = kwargs.get('encoding', 'utf-8') + sep = kwargs.get('sep', '\n') + buf = [] + try: + for filename in filenames: + with io.open(filename, encoding=encoding) as f: + buf.append(f.read()) + except IOError: + pass + return sep.join(buf) + +long_description = read('README.txt', 'CHANGES.txt') + + +class PyTest(TestCommand): + def finalize_options(self): + TestCommand.finalize_options(self) + self.test_args = [] + self.test_suite = True + + def run_tests(self): + import pytest + errcode = pytest.main(self.test_args) + sys.exit(errcode) + +setup( + name='clipperz', + version='0.1.0', + url='http://github.com/clipperz/password-manager/', + license='Apache Software License', + author='Jokajak', + tests_require=['pytest'], + install_requires=['Flask>=0.10.1', + 'Flask-SQLAlchemy>=1.0', + 'SQLAlchemy>=0.8.2', + 'Flask-Login', + 'Flask-KVSession', + ], + cmdclass={'test': PyTest}, + author_email='jokajak@gmail.com', + description='Clipperz password manager server', + long_description=long_description, + packages=['clipperz'], + include_package_data=True, + platforms='any', + test_suite='clipperz.test.test_clipperz', + classifiers=[ + 'Programming Language :: Python', + 'Development Status :: 4 - Beta', + 'Natural Language :: English', + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: OS Independent', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + ], + extras_require={ + 'testing': ['pytest'], + } +) diff --git a/scripts/builder/backends/flaskBuilder.py b/scripts/builder/backends/flaskBuilder.py new file mode 100755 index 0000000..613263a --- /dev/null +++ b/scripts/builder/backends/flaskBuilder.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +import os +import shutil +from scriptLanguageBuilder import ScriptLanguageBuilder + + +class FlaskBuilder(ScriptLanguageBuilder): + + def name(self): + return "Flask builder" + + def relativePath(self): + return 'flask' + + def createPackage(self): + super(FlaskBuilder, self).createPackage() diff --git a/scripts/builder/frontends/deltaBuilder.py b/scripts/builder/frontends/deltaBuilder.py index 24b523b..8f5223a 100644 --- a/scripts/builder/frontends/deltaBuilder.py +++ b/scripts/builder/frontends/deltaBuilder.py @@ -1,5 +1,5 @@ from frontendBuilder import FrontendBuilder -from scss import Scss +#from scss import Scss import os import shutil @@ -38,6 +38,7 @@ class DeltaBuilder(FrontendBuilder): return "" def preprocessCSS (self, targetFile): + from scss import Scss logging.basicConfig() scssVariables = {} scssCompiler = Scss( From 18a9936dfa0deb8d3d49ba4ddd34f12781cc980e Mon Sep 17 00:00:00 2001 From: jokajak Date: Mon, 20 Apr 2015 08:54:17 -0400 Subject: [PATCH 4/6] Updated flask backend README to include link to running instance --- backend/flask/src/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/flask/src/README.md b/backend/flask/src/README.md index 6555e72..57bc376 100644 --- a/backend/flask/src/README.md +++ b/backend/flask/src/README.md @@ -5,3 +5,5 @@ A flask based backend for the Clipperz (https://clipperz.is) Password Manager. T Running ------- Once you have built the backend using the clipperz build process you can use run.sh to create an environment for testing against. The database will be created in the target directory which means it will be over-ridden every time you build. To change this you can specify a `DATABASE_URL` environment variable that points to another location. + +Once it is running, you can open the clipperz by going to `http://localhost:5000/` for instance (http://localhost:5000/delta) From b812ea4efbc382c972ef79ffa5d3020082474b7e Mon Sep 17 00:00:00 2001 From: Dario Chiappetta Date: Mon, 20 Apr 2015 18:18:22 +0200 Subject: [PATCH 5/6] Added Export feature --- .../js/Clipperz/PM/DataModel/DirectLogin.js | 2 + .../Clipperz/PM/DataModel/Record.Version.js | 29 ++ .../delta/js/Clipperz/PM/DataModel/Record.js | 59 ++++ .../UI/Components/ExtraFeatures/DataExport.js | 51 +++ .../Components/Panels/ExtraFeaturesPanel.js | 2 +- .../js/Clipperz/PM/UI/ExportController.js | 301 ++++++++++++++++++ .../delta/js/Clipperz/PM/UI/MainController.js | 10 + .../delta/properties/delta.properties.json | 2 + 8 files changed, 455 insertions(+), 1 deletion(-) create mode 100644 frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DataExport.js create mode 100644 frontend/delta/js/Clipperz/PM/UI/ExportController.js 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" ], From e329f6926a1d86943f5c44cc6c01942efcf8d9bd Mon Sep 17 00:00:00 2001 From: Giulio Cesare Solaroli Date: Thu, 7 May 2015 16:23:57 +0200 Subject: [PATCH 6/6] Reviewed and improved export feature --- frontend/delta/css/clipperz.css | 167 ++++--- frontend/delta/html/index_template.html | 1 + .../Clipperz/PM/DataModel/Record.Version.js | 5 +- .../delta/js/Clipperz/PM/DataModel/Record.js | 96 ++-- .../UI/Components/ExtraFeatures/DataExport.js | 71 ++- .../Components/ExtraFeatures/DeleteAccount.js | 26 +- .../UI/Components/ExtraFeatures/DevicePIN.js | 4 +- .../UI/Components/ExtraFeatures/Passphrase.js | 34 +- .../js/Clipperz/PM/UI/Components/Overlay.js | 23 +- .../Components/Panels/ExtraFeaturesPanel.js | 45 +- .../js/Clipperz/PM/UI/ExportController.js | 437 +++++++++--------- .../delta/js/Clipperz/PM/UI/MainController.js | 28 +- frontend/delta/js/FileSaver/Blob.js | 234 ++++++++++ frontend/delta/js/FileSaver/FileSaver.js | 271 +++++++++++ .../delta/properties/delta.properties.json | 3 + frontend/delta/scss/core/layout.scss | 19 +- frontend/delta/scss/core/overlay.scss | 17 + frontend/delta/scss/style/settingsPanel.scss | 188 +++++--- 18 files changed, 1195 insertions(+), 474 deletions(-) create mode 100644 frontend/delta/js/FileSaver/Blob.js create mode 100644 frontend/delta/js/FileSaver/FileSaver.js diff --git a/frontend/delta/css/clipperz.css b/frontend/delta/css/clipperz.css index 2c6fab1..16b9b6a 100644 --- a/frontend/delta/css/clipperz.css +++ b/frontend/delta/css/clipperz.css @@ -469,6 +469,21 @@ div.overlay { -ms-animation-delay: -0.0833s; -o-animation-delay: -0.0833s; animation-delay: -0.0833s; } + div.overlay .progressBar { + width: 100%; + background-color: #222; + height: 4px; + margin-top: 86px; + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + border-radius: 2px; } + div.overlay .progressBar .progress { + background-color: #999; + height: 4px; + display: block; + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + border-radius: 2px; } @-webkit-keyframes overlay-spin { from { @@ -851,7 +866,8 @@ html { -moz-flex: auto; -ms-flex: auto; flex: auto; - overflow: auto; } + overflow: scroll; + -webkit-overflow-scrolling: touch; } #extraFeaturesPanel .extraFeatureIndex footer { -webkit-box-flex: none; -webkit-flex: none; @@ -869,6 +885,12 @@ html { width: 100%; height: 100%; background-color: black; } + #extraFeaturesPanel .extraFeatureContent .extraFeature { + height: 100%; } + #extraFeaturesPanel .extraFeatureContent .extraFeature .content { + height: 100%; + overflow: scroll; + -webkit-overflow-scrolling: touch; } .container { height: 100%; @@ -2015,6 +2037,8 @@ span.count { background-color: #333; } #extraFeaturesPanel .extraFeatureIndex > div ul li > ul > li > div { padding: 4px; } + #extraFeaturesPanel .extraFeatureIndex > div ul li > ul > li.offlineCopy { + cursor: default; } #extraFeaturesPanel .extraFeatureIndex > div ul li h2 { font-weight: 300; font-size: 14pt; } @@ -2091,65 +2115,88 @@ span.count { #extraFeaturesPanel .extraFeatureContent .extraFeature h1 { font-size: 20pt; padding-bottom: 20px; } - #extraFeaturesPanel .extraFeatureContent form label { - display: none; } - #extraFeaturesPanel .extraFeatureContent form input { - display: block; - font-size: 18pt; - margin-bottom: 8px; - padding: 6px 10px; - border: 0px solid white; - width: 350px; - color: black; } - #extraFeaturesPanel .extraFeatureContent form input.invalid { - border: 0px solid #ff9900; - color: gray; } - #extraFeaturesPanel .extraFeatureContent form p { - display: -webkit-box; - display: -webkit-flex; - display: -moz-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-direction: normal; - -webkit-box-orient: horizontal; - -webkit-flex-direction: row; - -moz-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; } - #extraFeaturesPanel .extraFeatureContent form p input { - width: 30px; - -webkit-box-flex: auto; - -webkit-flex: auto; - -moz-box-flex: auto; - -moz-flex: auto; - -ms-flex: auto; - flex: auto; } - #extraFeaturesPanel .extraFeatureContent form p span { - -webkit-box-flex: auto; - -webkit-flex: auto; - -moz-box-flex: auto; - -moz-flex: auto; - -ms-flex: auto; - flex: auto; - font-size: 12pt; } - #extraFeaturesPanel .extraFeatureContent form button { - font-family: "clipperz-font"; - color: white; - font-size: 14pt; - border: 0px; - margin-top: 20px; - padding: 6px 10px; - border: 1px solid white; - background-color: #ff9900; - -webkit-transition: background-color font-weight 0.2s linear; - -moz-transition: background-color font-weight 0.2s linear; - -o-transition: background-color font-weight 0.2s linear; - -ms-transition: background-color font-weight 0.2s linear; - transition: background-color font-weight 0.2s linear; } - #extraFeaturesPanel .extraFeatureContent form button:disabled { - font-weight: 100; - background-color: #c0c0c0; - cursor: default; } + #extraFeaturesPanel .extraFeatureContent .extraFeature form label { + display: none; } + #extraFeaturesPanel .extraFeatureContent .extraFeature form input { + display: block; + font-size: 18pt; + margin-bottom: 8px; + padding: 6px 10px; + border: 0px solid white; + width: 350px; + color: black; } + #extraFeaturesPanel .extraFeatureContent .extraFeature form input.invalid { + border: 0px solid #ff9900; + color: gray; } + #extraFeaturesPanel .extraFeatureContent .extraFeature form p { + display: -webkit-box; + display: -webkit-flex; + display: -moz-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-direction: normal; + -webkit-box-orient: horizontal; + -webkit-flex-direction: row; + -moz-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; } + #extraFeaturesPanel .extraFeatureContent .extraFeature form p input { + width: 30px; + -webkit-box-flex: auto; + -webkit-flex: auto; + -moz-box-flex: auto; + -moz-flex: auto; + -ms-flex: auto; + flex: auto; } + #extraFeaturesPanel .extraFeatureContent .extraFeature form p span { + -webkit-box-flex: auto; + -webkit-flex: auto; + -moz-box-flex: auto; + -moz-flex: auto; + -ms-flex: auto; + flex: auto; + font-size: 12pt; } + #extraFeaturesPanel .extraFeatureContent .extraFeature form button { + font-family: "clipperz-font"; + color: white; + font-size: 14pt; + border: 0px; + margin-top: 20px; + padding: 6px 10px; + border: 1px solid white; + background-color: #ff9900; + -webkit-transition: background-color font-weight 0.2s linear; + -moz-transition: background-color font-weight 0.2s linear; + -o-transition: background-color font-weight 0.2s linear; + -ms-transition: background-color font-weight 0.2s linear; + transition: background-color font-weight 0.2s linear; } + #extraFeaturesPanel .extraFeatureContent .extraFeature form button:disabled { + font-weight: 100; + background-color: #c0c0c0; + cursor: default; } + #extraFeaturesPanel .extraFeatureContent .extraFeature ul { + color: white; } + #extraFeaturesPanel .extraFeatureContent .extraFeature ul li { + padding-bottom: 40px; } + #extraFeaturesPanel .extraFeatureContent .extraFeature h3 { + font-size: 18pt; } + #extraFeaturesPanel .extraFeatureContent .extraFeature .description { + max-width: 500px; + padding: 10px 0px 20px 0px; } + #extraFeaturesPanel .extraFeatureContent .extraFeature .description p { + font-size: 10pt; + margin-bottom: 7px; + line-height: 1.4em; + color: #bbb; } + #extraFeaturesPanel .extraFeatureContent .extraFeature .description p em { + text-decoration: underline; } + #extraFeaturesPanel .extraFeatureContent .extraFeature .button { + display: inline; + color: white; + background-color: #ff9900; + font-size: 14pt; + border: 1px solid white; + padding: 6px 10px; } .mainPage.narrow #extraFeaturesPanel .extraFeatureContent header { display: block; diff --git a/frontend/delta/html/index_template.html b/frontend/delta/html/index_template.html index 6f6c166..4514f08 100644 --- a/frontend/delta/html/index_template.html +++ b/frontend/delta/html/index_template.html @@ -85,6 +85,7 @@ Clipperz_normalizedNewLine = '\x0d\x0a'; + loading diff --git a/frontend/delta/js/Clipperz/PM/DataModel/Record.Version.js b/frontend/delta/js/Clipperz/PM/DataModel/Record.Version.js index da960b9..db91fad 100644 --- a/frontend/delta/js/Clipperz/PM/DataModel/Record.Version.js +++ b/frontend/delta/js/Clipperz/PM/DataModel/Record.Version.js @@ -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(); diff --git a/frontend/delta/js/Clipperz/PM/DataModel/Record.js b/frontend/delta/js/Clipperz/PM/DataModel/Record.js index aa3311b..5abe8c5 100644 --- a/frontend/delta/js/Clipperz/PM/DataModel/Record.js +++ b/frontend/delta/js/Clipperz/PM/DataModel/Record.js @@ -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; +}; diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DataExport.js b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DataExport.js index 13ebe3f..fb2ceb4 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DataExport.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DataExport.js @@ -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 it’s 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"), + ]) +*/ + ]) + ]) ]); }, diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DeleteAccount.js b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DeleteAccount.js index 3c71f0d..fc1d976 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DeleteAccount.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DeleteAccount.js @@ -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) + ]) ]) ]); }, diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DevicePIN.js b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DevicePIN.js index 45c835e..9c8e938 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DevicePIN.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DevicePIN.js @@ -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']) + ]) ]); }, diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/Passphrase.js b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/Passphrase.js index a286d3a..49f7aeb 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/Passphrase.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/Passphrase.js @@ -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"), + ]) + ]) ]); }, diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/Overlay.js b/frontend/delta/js/Clipperz/PM/UI/Components/Overlay.js index f9f6bcd..01d8a77 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/Overlay.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/Overlay.js @@ -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; }, 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 92609e5..f27ccd7 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/Panels/ExtraFeaturesPanel.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/Panels/ExtraFeaturesPanel.js @@ -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, ]); } diff --git a/frontend/delta/js/Clipperz/PM/UI/ExportController.js b/frontend/delta/js/Clipperz/PM/UI/ExportController.js index 11c5207..a4bd81c 100644 --- a/frontend/delta/js/Clipperz/PM/UI/ExportController.js +++ b/frontend/delta/js/Clipperz/PM/UI/ExportController.js @@ -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 '' + title + '' + MochiKit.DOM.toHTML(body) + ''; }, - //----------------------------------------------------------------------------- - - '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, - "

Error

"+ - "

The following error occured while exporting your data:

"+ - ""+errorMessage+"" - ); + 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(); -}; diff --git a/frontend/delta/js/Clipperz/PM/UI/MainController.js b/frontend/delta/js/Clipperz/PM/UI/MainController.js index 7b8a5cb..b26adbb 100644 --- a/frontend/delta/js/Clipperz/PM/UI/MainController.js +++ b/frontend/delta/js/Clipperz/PM/UI/MainController.js @@ -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; }, //---------------------------------------------------------------------------- diff --git a/frontend/delta/js/FileSaver/Blob.js b/frontend/delta/js/FileSaver/Blob.js new file mode 100644 index 0000000..2e2d360 --- /dev/null +++ b/frontend/delta/js/FileSaver/Blob.js @@ -0,0 +1,234 @@ +/* + +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/. + +*/ + +/* Blob.js + * A Blob implementation. + * 2014-07-24 + * + * By Eli Grey, http://eligrey.com + * By Devin Samarin, https://github.com/dsamarin + * License: X11/MIT + * See https://github.com/eligrey/Blob.js/blob/master/LICENSE.md + */ + +/*global self, unescape */ +/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true, + plusplus: true */ + +/*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */ + +(function (view) { + "use strict"; + + view.URL = view.URL || view.webkitURL; + + if (view.Blob && view.URL) { + try { + new Blob; + return; + } catch (e) {} + } + + // Internally we use a BlobBuilder implementation to base Blob off of + // in order to support older browsers that only have BlobBuilder + var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) { + var + get_class = function(object) { + return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1]; + } + , FakeBlobBuilder = function BlobBuilder() { + this.data = []; + } + , FakeBlob = function Blob(data, type, encoding) { + this.data = data; + this.size = data.length; + this.type = type; + this.encoding = encoding; + } + , FBB_proto = FakeBlobBuilder.prototype + , FB_proto = FakeBlob.prototype + , FileReaderSync = view.FileReaderSync + , FileException = function(type) { + this.code = this[this.name = type]; + } + , file_ex_codes = ( + "NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR " + + "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR" + ).split(" ") + , file_ex_code = file_ex_codes.length + , real_URL = view.URL || view.webkitURL || view + , real_create_object_URL = real_URL.createObjectURL + , real_revoke_object_URL = real_URL.revokeObjectURL + , URL = real_URL + , btoa = view.btoa + , atob = view.atob + + , ArrayBuffer = view.ArrayBuffer + , Uint8Array = view.Uint8Array + + , origin = /^[\w-]+:\/*\[?[\w\.:-]+\]?(?::[0-9]+)?/ + ; + FakeBlob.fake = FB_proto.fake = true; + while (file_ex_code--) { + FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1; + } + // Polyfill URL + if (!real_URL.createObjectURL) { + URL = view.URL = function(uri) { + var + uri_info = document.createElementNS("http://www.w3.org/1999/xhtml", "a") + , uri_origin + ; + uri_info.href = uri; + if (!("origin" in uri_info)) { + if (uri_info.protocol.toLowerCase() === "data:") { + uri_info.origin = null; + } else { + uri_origin = uri.match(origin); + uri_info.origin = uri_origin && uri_origin[1]; + } + } + return uri_info; + }; + } + URL.createObjectURL = function(blob) { + var + type = blob.type + , data_URI_header + ; + if (type === null) { + type = "application/octet-stream"; + } + if (blob instanceof FakeBlob) { + data_URI_header = "data:" + type; + if (blob.encoding === "base64") { + return data_URI_header + ";base64," + blob.data; + } else if (blob.encoding === "URI") { + return data_URI_header + "," + decodeURIComponent(blob.data); + } if (btoa) { + return data_URI_header + ";base64," + btoa(blob.data); + } else { + return data_URI_header + "," + encodeURIComponent(blob.data); + } + } else if (real_create_object_URL) { + return real_create_object_URL.call(real_URL, blob); + } + }; + URL.revokeObjectURL = function(object_URL) { + if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) { + real_revoke_object_URL.call(real_URL, object_URL); + } + }; + FBB_proto.append = function(data/*, endings*/) { + var bb = this.data; + // decode data to a binary string + if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) { + var + str = "" + , buf = new Uint8Array(data) + , i = 0 + , buf_len = buf.length + ; + for (; i < buf_len; i++) { + str += String.fromCharCode(buf[i]); + } + bb.push(str); + } else if (get_class(data) === "Blob" || get_class(data) === "File") { + if (FileReaderSync) { + var fr = new FileReaderSync; + bb.push(fr.readAsBinaryString(data)); + } else { + // async FileReader won't work as BlobBuilder is sync + throw new FileException("NOT_READABLE_ERR"); + } + } else if (data instanceof FakeBlob) { + if (data.encoding === "base64" && atob) { + bb.push(atob(data.data)); + } else if (data.encoding === "URI") { + bb.push(decodeURIComponent(data.data)); + } else if (data.encoding === "raw") { + bb.push(data.data); + } + } else { + if (typeof data !== "string") { + data += ""; // convert unsupported types to strings + } + // decode UTF-16 to binary string + bb.push(unescape(encodeURIComponent(data))); + } + }; + FBB_proto.getBlob = function(type) { + if (!arguments.length) { + type = null; + } + return new FakeBlob(this.data.join(""), type, "raw"); + }; + FBB_proto.toString = function() { + return "[object BlobBuilder]"; + }; + FB_proto.slice = function(start, end, type) { + var args = arguments.length; + if (args < 3) { + type = null; + } + return new FakeBlob( + this.data.slice(start, args > 1 ? end : this.data.length) + , type + , this.encoding + ); + }; + FB_proto.toString = function() { + return "[object Blob]"; + }; + FB_proto.close = function() { + this.size = 0; + delete this.data; + }; + return FakeBlobBuilder; + }(view)); + + view.Blob = function(blobParts, options) { + var type = options ? (options.type || "") : ""; + var builder = new BlobBuilder(); + if (blobParts) { + for (var i = 0, len = blobParts.length; i < len; i++) { + if (Uint8Array && blobParts[i] instanceof Uint8Array) { + builder.append(blobParts[i].buffer); + } + else { + builder.append(blobParts[i]); + } + } + } + var blob = builder.getBlob(type); + if (!blob.slice && blob.webkitSlice) { + blob.slice = blob.webkitSlice; + } + return blob; + }; + + var getPrototypeOf = Object.getPrototypeOf || function(object) { + return object.__proto__; + }; + view.Blob.prototype = getPrototypeOf(new view.Blob()); +}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this)); \ No newline at end of file diff --git a/frontend/delta/js/FileSaver/FileSaver.js b/frontend/delta/js/FileSaver/FileSaver.js new file mode 100644 index 0000000..c43c5f6 --- /dev/null +++ b/frontend/delta/js/FileSaver/FileSaver.js @@ -0,0 +1,271 @@ +/* + +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/. + +*/ + +/* FileSaver.js + * A saveAs() FileSaver implementation. + * 2015-03-04 + * + * By Eli Grey, http://eligrey.com + * License: X11/MIT + * See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md + */ + +/*global self */ +/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */ + +/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ + +var saveAs = saveAs + // IE 10+ (native saveAs) + || (typeof navigator !== "undefined" && + navigator.msSaveOrOpenBlob && navigator.msSaveOrOpenBlob.bind(navigator)) + // Everyone else + || (function(view) { + "use strict"; + // IE <10 is explicitly unsupported + if (typeof navigator !== "undefined" && + /MSIE [1-9]\./.test(navigator.userAgent)) { + return; + } + var + doc = view.document + // only get URL when necessary in case Blob.js hasn't overridden it yet + , get_URL = function() { + return view.URL || view.webkitURL || view; + } + , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a") + , can_use_save_link = "download" in save_link + , click = function(node) { + var event = doc.createEvent("MouseEvents"); + event.initMouseEvent( + "click", true, false, view, 0, 0, 0, 0, 0 + , false, false, false, false, 0, null + ); + node.dispatchEvent(event); + } + , webkit_req_fs = view.webkitRequestFileSystem + , req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem + , throw_outside = function(ex) { + (view.setImmediate || view.setTimeout)(function() { + throw ex; + }, 0); + } + , force_saveable_type = "application/octet-stream" + , fs_min_size = 0 + // See https://code.google.com/p/chromium/issues/detail?id=375297#c7 and + // https://github.com/eligrey/FileSaver.js/commit/485930a#commitcomment-8768047 + // for the reasoning behind the timeout and revocation flow + , arbitrary_revoke_timeout = 500 // in ms + , revoke = function(file) { + var revoker = function() { + if (typeof file === "string") { // file is an object URL + get_URL().revokeObjectURL(file); + } else { // file is a File + file.remove(); + } + }; + if (view.chrome) { + revoker(); + } else { + setTimeout(revoker, arbitrary_revoke_timeout); + } + } + , dispatch = function(filesaver, event_types, event) { + event_types = [].concat(event_types); + var i = event_types.length; + while (i--) { + var listener = filesaver["on" + event_types[i]]; + if (typeof listener === "function") { + try { + listener.call(filesaver, event || filesaver); + } catch (ex) { + throw_outside(ex); + } + } + } + } + , FileSaver = function(blob, name) { + // First try a.download, then web filesystem, then object URLs + var + filesaver = this + , type = blob.type + , blob_changed = false + , object_url + , target_view + , dispatch_all = function() { + dispatch(filesaver, "writestart progress write writeend".split(" ")); + } + // on any filesys errors revert to saving with object URLs + , fs_error = function() { + // don't create more object URLs than needed + if (blob_changed || !object_url) { + object_url = get_URL().createObjectURL(blob); + } + if (target_view) { + target_view.location.href = object_url; + } else { + var new_tab = view.open(object_url, "_blank"); + if (new_tab == undefined && typeof safari !== "undefined") { + //Apple do not allow window.open, see http://bit.ly/1kZffRI + view.location.href = object_url + } + } + filesaver.readyState = filesaver.DONE; + dispatch_all(); + revoke(object_url); + } + , abortable = function(func) { + return function() { + if (filesaver.readyState !== filesaver.DONE) { + return func.apply(this, arguments); + } + }; + } + , create_if_not_found = {create: true, exclusive: false} + , slice + ; + filesaver.readyState = filesaver.INIT; + if (!name) { + name = "download"; + } + if (can_use_save_link) { + object_url = get_URL().createObjectURL(blob); + save_link.href = object_url; + save_link.download = name; + click(save_link); + filesaver.readyState = filesaver.DONE; + dispatch_all(); + revoke(object_url); + return; + } + // prepend BOM for UTF-8 XML and text/plain types + if (/^\s*(?:text\/(?:plain|xml)|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) { + blob = new Blob(["\ufeff", blob], {type: blob.type}); + } + // Object and web filesystem URLs have a problem saving in Google Chrome when + // viewed in a tab, so I force save with application/octet-stream + // http://code.google.com/p/chromium/issues/detail?id=91158 + // Update: Google errantly closed 91158, I submitted it again: + // https://code.google.com/p/chromium/issues/detail?id=389642 + if (view.chrome && type && type !== force_saveable_type) { + slice = blob.slice || blob.webkitSlice; + blob = slice.call(blob, 0, blob.size, force_saveable_type); + blob_changed = true; + } + // Since I can't be sure that the guessed media type will trigger a download + // in WebKit, I append .download to the filename. + // https://bugs.webkit.org/show_bug.cgi?id=65440 + if (webkit_req_fs && name !== "download") { + name += ".download"; + } + if (type === force_saveable_type || webkit_req_fs) { + target_view = view; + } + if (!req_fs) { + fs_error(); + return; + } + fs_min_size += blob.size; + req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) { + fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) { + var save = function() { + dir.getFile(name, create_if_not_found, abortable(function(file) { + file.createWriter(abortable(function(writer) { + writer.onwriteend = function(event) { + target_view.location.href = file.toURL(); + filesaver.readyState = filesaver.DONE; + dispatch(filesaver, "writeend", event); + revoke(file); + }; + writer.onerror = function() { + var error = writer.error; + if (error.code !== error.ABORT_ERR) { + fs_error(); + } + }; + "writestart progress write abort".split(" ").forEach(function(event) { + writer["on" + event] = filesaver["on" + event]; + }); + writer.write(blob); + filesaver.abort = function() { + writer.abort(); + filesaver.readyState = filesaver.DONE; + }; + filesaver.readyState = filesaver.WRITING; + }), fs_error); + }), fs_error); + }; + dir.getFile(name, {create: false}, abortable(function(file) { + // delete file if it already exists + file.remove(); + save(); + }), abortable(function(ex) { + if (ex.code === ex.NOT_FOUND_ERR) { + save(); + } else { + fs_error(); + } + })); + }), fs_error); + }), fs_error); + } + , FS_proto = FileSaver.prototype + , saveAs = function(blob, name) { + return new FileSaver(blob, name); + } + ; + FS_proto.abort = function() { + var filesaver = this; + filesaver.readyState = filesaver.DONE; + dispatch(filesaver, "abort"); + }; + FS_proto.readyState = FS_proto.INIT = 0; + FS_proto.WRITING = 1; + FS_proto.DONE = 2; + + FS_proto.error = + FS_proto.onwritestart = + FS_proto.onprogress = + FS_proto.onwrite = + FS_proto.onabort = + FS_proto.onerror = + FS_proto.onwriteend = + null; + + return saveAs; +}( + typeof self !== "undefined" && self + || typeof window !== "undefined" && window + || this.content +)); +// `self` is undefined in Firefox for Android content script context +// while `this` is nsIContentFrameMessageManager +// with an attribute `content` that corresponds to the window + +if (typeof module !== "undefined" && module.exports) { + module.exports.saveAs = saveAs; +} else if ((typeof define !== "undefined" && define !== null) && (define.amd != null)) { + define([], function() { + return saveAs; + }); +} \ No newline at end of file diff --git a/frontend/delta/properties/delta.properties.json b/frontend/delta/properties/delta.properties.json index bbfa4a4..b53c099 100644 --- a/frontend/delta/properties/delta.properties.json +++ b/frontend/delta/properties/delta.properties.json @@ -58,6 +58,9 @@ "-- Modernizr/modernizr-2.8.2.js", "OnMediaQuery/onmediaquery-0.2.0.js", + "FileSaver/Blob.js", + "FileSaver/FileSaver.js", + "-- IT WOULD BE NICE TO BE ABLE TO GET RID OF THESE IMPORTS", "Clipperz/YUI/Utils.js", "Clipperz/YUI/DomHelper.js", diff --git a/frontend/delta/scss/core/layout.scss b/frontend/delta/scss/core/layout.scss index ce969d3..271cbf0 100644 --- a/frontend/delta/scss/core/layout.scss +++ b/frontend/delta/scss/core/layout.scss @@ -209,9 +209,8 @@ html { & > div { @include flex(auto); - - overflow: auto; - +// overflow: auto; + @include overflow-scroll(); } footer { @@ -226,6 +225,20 @@ html { height: 100%; // background-color: rgba( 0, 0, 0, 0.95); background-color: black; + + .extraFeature { +// @include flexbox(); + height: 100%; + + h1 { +// @include flex(none); + } + .content { +// @include flex(auto); + height: 100%; + @include overflow-scroll(); + } + } } } diff --git a/frontend/delta/scss/core/overlay.scss b/frontend/delta/scss/core/overlay.scss index 5ba1c05..ce5ea83 100644 --- a/frontend/delta/scss/core/overlay.scss +++ b/frontend/delta/scss/core/overlay.scss @@ -126,6 +126,23 @@ div.overlay { div.bar11 {@include transform(300deg, 0, -142%); @include animation-delay(-0.16670s);} div.bar12 {@include transform(330deg, 0, -142%); @include animation-delay(-0.08330s);} } + + .progressBar { +// display: block; + width: 100%; + background-color: #222; + height: 4px; + margin-top: 86px; + @include border-radius(2px); + + .progress { + background-color: #999; +// width: 70%; + height: 4px; + display: block; + @include border-radius(2px); + } + } } //======================================================== diff --git a/frontend/delta/scss/style/settingsPanel.scss b/frontend/delta/scss/style/settingsPanel.scss index a5126e9..8a8ce7a 100644 --- a/frontend/delta/scss/style/settingsPanel.scss +++ b/frontend/delta/scss/style/settingsPanel.scss @@ -86,6 +86,10 @@ refer to http://www.clipperz.com. & > div { padding: 4px; } + + &.offlineCopy { + cursor: default; + } } &.open { @@ -178,6 +182,118 @@ refer to http://www.clipperz.com. font-size: 20pt; padding-bottom: 20px; } + + form { + + label { + display: none; + } + + input { + $border-size: 0px; // 2px; + + display: block; + font-size: 18pt; + margin-bottom: 8px; + padding: (6px - $border-size) (10px - $border-size); + border: $border-size solid white; + width: 350px; + color: black; + + &.invalid { + border: $border-size solid $clipperz-orange; + color: gray; + } + } + + p { + @include flexbox; + @include flex-direction(row); + + input { + width: 30px; + @include flex(auto); + } + + span { + @include flex(auto); + font-size: 12pt; + } + } + + button { + font-family: "clipperz-font"; + + color: white; + font-size: 14pt; + border: 0px; + + margin-top: 20px; + padding: 6px 10px; + + border: 1px solid white; + background-color: $main-color; + @include transition(background-color font-weight, 0.2s, linear); + + &:hover { + }; + + &:disabled { + font-weight: 100; + background-color: #c0c0c0; + cursor: default; + + &:hover { + }; + } + } +// input.valid:focus { +// border: 2px solid $clipperz-blue; +// } + } + + ul { + color: white; + + li { + padding-bottom: 40px; + } + } + + h3 { + font-size: 18pt; + } + + .description { + max-width: 500px; + padding: 10px 0px 20px 0px; + + p { + font-size: 10pt; + margin-bottom: 7px; + line-height: 1.4em; + color:#bbb; + + em { + text-decoration: underline; + } + } + } + + .button { + display: inline; + + color: white; + background-color: $main-color; + + font-size: 14pt; + + border: 1px solid white; + padding: 6px 10px; + + &:after { + }; + } } @@ -211,78 +327,6 @@ refer to http://www.clipperz.com. } */ - form { - - label { - display: none; - } - - input { - $border-size: 0px; // 2px; - - display: block; - font-size: 18pt; - margin-bottom: 8px; - padding: (6px - $border-size) (10px - $border-size); - border: $border-size solid white; - width: 350px; - color: black; - - &.invalid { - border: $border-size solid $clipperz-orange; - color: gray; - } - } - - p { - @include flexbox; - @include flex-direction(row); - - input { - width: 30px; - @include flex(auto); - } - - span { - @include flex(auto); - font-size: 12pt; - } - } - - button { - font-family: "clipperz-font"; -// min-height: 48px; -// min-width: 48px; - - color: white; -// font-size: 24pt; - font-size: 14pt; -// font-weight: 500; - border: 0px; - - margin-top: 20px; - padding: 6px 10px; - - border: 1px solid white; - background-color: $main-color; - @include transition(background-color font-weight, 0.2s, linear); - - &:hover { - }; - - &:disabled { - font-weight: 100; - background-color: #c0c0c0; - cursor: default; - - &:hover { - }; - } - } -// input.valid:focus { -// border: 2px solid $clipperz-blue; -// } - } } }