diff --git a/frontend/delta/html/index_template.html b/frontend/delta/html/index_template.html index 46da622..8d034c2 100644 --- a/frontend/delta/html/index_template.html +++ b/frontend/delta/html/index_template.html @@ -63,6 +63,7 @@ Clipperz_normalizedNewLine = '\x0d\x0a';
+
diff --git a/frontend/delta/js/Clipperz/PM/DataModel/User.Header.Legacy.js b/frontend/delta/js/Clipperz/PM/DataModel/User.Header.Legacy.js index 879d988..0001c3d 100644 --- a/frontend/delta/js/Clipperz/PM/DataModel/User.Header.Legacy.js +++ b/frontend/delta/js/Clipperz/PM/DataModel/User.Header.Legacy.js @@ -35,6 +35,8 @@ Clipperz.PM.DataModel.User.Header.Legacy = function(args) { this._records = null; // this._directLogins = null; +Clipperz.log("Clipperz.PM.DataModel.User.Header.Legacy: Legacy header in use!"); + return this; } diff --git a/frontend/delta/js/Clipperz/PM/DataModel/User.Header.Preferences.js b/frontend/delta/js/Clipperz/PM/DataModel/User.Header.Preferences.js index 152ec8c..61deac0 100644 --- a/frontend/delta/js/Clipperz/PM/DataModel/User.Header.Preferences.js +++ b/frontend/delta/js/Clipperz/PM/DataModel/User.Header.Preferences.js @@ -29,11 +29,10 @@ if (typeof(Clipperz.PM.DataModel.User.Header) == 'undefined') { Clipperz.PM.Data Clipperz.PM.DataModel.User.Header.Preferences = function(args) { Clipperz.PM.DataModel.User.Header.Preferences.superclass.constructor.apply(this, arguments); - + return this; } - Clipperz.Base.extend(Clipperz.PM.DataModel.User.Header.Preferences, Clipperz.PM.DataModel.EncryptedRemoteObject, { 'toString': function() { @@ -41,8 +40,42 @@ Clipperz.Base.extend(Clipperz.PM.DataModel.User.Header.Preferences, Clipperz.PM. }, //------------------------------------------------------------------------- + + 'mergeDefaultPreferences': function(somePreferences) { + var result; + + result = new Clipperz.KeyValueObjectStore(); + + result.setValues(MochiKit.Base.updatetree( + Clipperz.Base.deepClone(Clipperz.PM.DataModel.User.Header.Preferences.defaultPreferences), + somePreferences + )); + + return result; + }, + + 'getPreferences': function() { + return Clipperz.Async.callbacks("User.Header.Preferences.getPreferences", [ + MochiKit.Base.method(this, 'values'), + MochiKit.Base.method(this, 'mergeDefaultPreferences') + ], {trace:false}); + }, + + 'getPreference': function(aKey) { + return Clipperz.Async.callbacks("User.Header.Preferences.getPreference", [ + MochiKit.Base.method(this, 'getPreferences'), + MochiKit.Base.methodcaller('getValue', aKey) + ], {trace:false}); + }, + //========================================================================= __syntaxFix__: "syntax fix" }); +Clipperz.PM.DataModel.User.Header.Preferences.defaultPreferences = { + 'lock': { + 'timeoutInMinutes': 10 + }, + 'shouldShowDonationPanel': true +}; \ No newline at end of file diff --git a/frontend/delta/js/Clipperz/PM/DataModel/User.js b/frontend/delta/js/Clipperz/PM/DataModel/User.js index 5e9c402..19c2698 100644 --- a/frontend/delta/js/Clipperz/PM/DataModel/User.js +++ b/frontend/delta/js/Clipperz/PM/DataModel/User.js @@ -394,7 +394,9 @@ Clipperz.Base.extend(Clipperz.PM.DataModel.User, Object, { 'lock': function () { return Clipperz.Async.callbacks("User.lock", [ - MochiKit.Base.method(this, 'deleteAllCleanTextData') + MochiKit.Base.method(this.connection(), 'logout'), + MochiKit.Base.method(this, 'deleteAllCleanTextData'), + MochiKit.Base.method(this, 'setPassphraseFunction', function() {throw("No passphrase set.")}) ], {trace:false}); }, @@ -906,6 +908,30 @@ console.log("Warning: User.recordWithLabel('" + aLabel + "') is returning more t ], {trace:false}); }, + //------------------------------------------------------------------------- + + 'getPreferences': function() { + return Clipperz.Async.callbacks("User.getPreferences", [ + MochiKit.Base.method(this, 'getHeaderIndex', 'preferences'), + MochiKit.Base.methodcaller('getPreferences') + ], {trace:false}); + }, + + 'getPreference': function(aKey) { + return Clipperz.Async.callbacks("User.getPreference", [ + MochiKit.Base.method(this, 'getHeaderIndex', 'preferences'), + MochiKit.Base.methodcaller('getPreference', aKey) + ], {trace:false}); + }, + + 'setPreference': function(aKey, aValue) { + return Clipperz.Async.callbacks("User.setPreference", [ + MochiKit.Base.method(this, 'getHeaderIndex', 'preferences'), + MochiKit.Base.methodcaller('setValue', aKey, aValue), + MochiKit.Base.method(this, 'saveChanges') + ], {trace:false}); + }, + //========================================================================= 'hasPendingChanges': function () { diff --git a/frontend/delta/js/Clipperz/PM/Proxy/Proxy.Offline.DataStore.js b/frontend/delta/js/Clipperz/PM/Proxy/Proxy.Offline.DataStore.js index f647f1a..aad0ca4 100644 --- a/frontend/delta/js/Clipperz/PM/Proxy/Proxy.Offline.DataStore.js +++ b/frontend/delta/js/Clipperz/PM/Proxy/Proxy.Offline.DataStore.js @@ -234,36 +234,44 @@ Clipperz.Base.extend(Clipperz.PM.Proxy.Offline.DataStore, Object, { //========================================================================= 'processMessage': function (aFunctionName, someParameters) { - var result; - var connection; + var deferredResult; + + try { + var result; + var connection; - connection = this.getConnectionForRequest(aFunctionName, someParameters); + connection = this.getConnectionForRequest(aFunctionName, someParameters); - switch(aFunctionName) { - case 'knock': - result = this._knock(connection, someParameters); - break; - case 'registration': - this.checkToll(aFunctionName, someParameters); - result = this._registration(connection, someParameters.parameters); - break; - case 'handshake': - this.checkToll(aFunctionName, someParameters); - result = this._handshake(connection, someParameters.parameters); - break; - case 'message': - this.checkToll(aFunctionName, someParameters); - result = this._message(connection, someParameters.parameters); - break; - case 'logout': - this._currentStaticConnection = null; - result = this._logout(connection, someParameters.parameters); - break; + switch(aFunctionName) { + case 'knock': + result = this._knock(connection, someParameters); + break; + case 'registration': + this.checkToll(aFunctionName, someParameters); + result = this._registration(connection, someParameters.parameters); + break; + case 'handshake': + this.checkToll(aFunctionName, someParameters); + result = this._handshake(connection, someParameters.parameters); + break; + case 'message': + this.checkToll(aFunctionName, someParameters); + result = this._message(connection, someParameters.parameters); + break; + case 'logout': + this._currentStaticConnection = null; + result = this._logout(connection, someParameters.parameters); + break; + } + + this.storeConnectionForRequestWithConnectionAndResponse(aFunctionName, someParameters, connection, result); + + deferredResult = MochiKit.Async.succeed(result); + } catch (exception) { + deferredResult = MochiKit.Async.fail(exception); } - - this.storeConnectionForRequestWithConnectionAndResponse(aFunctionName, someParameters, connection, result); - - return MochiKit.Async.succeed(result); + + return deferredResult; }, //========================================================================= @@ -395,6 +403,7 @@ Clipperz.Base.extend(Clipperz.PM.Proxy.Offline.DataStore, Object, { ); result['M2'] = M2; result['accountInfo'] = aConnection['userData']['accountInfo']; + result['lock'] = '<>'; } else { throw new Error("Client checksum verification failed! Expected <" + M1 + ">, received <" + someParameters.parameters.M1 + ">.", "Error"); } @@ -438,7 +447,7 @@ Clipperz.Base.extend(Clipperz.PM.Proxy.Offline.DataStore, Object, { result = { result: result, - toll: this.getTollForRequestType(nextTollRequestType) + toll: this.getTollForRequestType(nextTollRequestType), } return result; @@ -449,6 +458,10 @@ Clipperz.Base.extend(Clipperz.PM.Proxy.Offline.DataStore, Object, { '_message': function(aConnection, someParameters) { var result; + if (MochiKit.Base.keys(aConnection).length==0) { + throw('Trying to communicate without an active connection'); + } + result = {}; //===================================================================== diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/Cards/View.js b/frontend/delta/js/Clipperz/PM/UI/Components/Cards/View.js index 9d65eb7..a231556 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/Cards/View.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/Cards/View.js @@ -219,7 +219,8 @@ Clipperz.PM.UI.Components.Cards.ViewClass = React.createClass({ Clipperz.PM.UI.Components.Cards.TextArea({ // React.DOM.textarea({ 'readOnly': true, - 'onClick': function(e) { e.target.select(); }, + // 'onMouseUp': function(e) { e.target.focus(); e.target.select(); e.stopPropagation(); e.preventDefault();}, + 'onClick': function(e) { e.target.focus(); e.target.select(); e.target.selectionStart = 0; e.target.selectionEnd = e.target.value.length; e.stopPropagation(); e.preventDefault(); }, 'className':Clipperz.PM.UI.Components.classNames(cardFieldValueClasses), 'value': aField['value'], 'rows': 1 diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/OTP.js b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/OTP.js index 389cd6e..e710945 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/OTP.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/OTP.js @@ -26,8 +26,6 @@ Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures'); Clipperz.PM.UI.Components.ExtraFeatures.OTPClass = React.createClass({ - // TODO: add print button!!!! - getInitialState: function() { return { // 'selectedOTPs': [], diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/Preferences.js b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/Preferences.js new file mode 100644 index 0000000..7c948c1 --- /dev/null +++ b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/Preferences.js @@ -0,0 +1,150 @@ +/* + +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.PreferencesClass = React.createClass({ + + getInitialState: function() { + return { + 'preferenceBeingEdited': null, + 'preferenceValue': '', + }; + }, + + propTypes: { + }, + + //========================================================================= + + setEditedPreference: function(aKey, aValue) { + this.setState({ + 'preferenceBeingEdited': aKey, + 'preferenceValue': aValue + }); + }, + + handleChange: function(anEvent) { + var newState = this.state; + + newState['preferenceValue'] = anEvent.target.value; + + this.setState(newState); + }, + + handleSave: function() { + MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'setPreference', this.state['preferenceBeingEdited'], this.state['preferenceValue']); + this.setEditedPreference(null, ''); + }, + + handleCancel: function() { + this.setEditedPreference(null, ''); + }, + + handleKeyPressed: function(anEvent) { + switch (anEvent.keyCode) { + case 9: // tab + this.handleSave(); + // TODO: edit next preference + break; + case 13: // enter + this.handleSave(); + break; + case 27: // escape + this.handleCancel(); + break; + } + }, + + //========================================================================= + + renderPreferenceValueElement: function(aKey) { + var result; + + var preferenceValue = this.props.userInfo.preferences.getValue(aKey); + + if (this.state.preferenceBeingEdited == aKey) { + result = React.DOM.input({ + 'autoFocus': true, + 'key': aKey, + 'type': 'text', + 'value': this.state.preferenceValue, + 'onChange': this.handleChange, + 'onKeyDown': MochiKit.Base.method(this, 'handleKeyPressed'), + }); + } else { + result = React.DOM.p({ + 'className': 'preferenceValue', + 'onClick': MochiKit.Base.method(this, 'setEditedPreference', aKey, preferenceValue) + }, preferenceValue); + } + + return result; + }, + + renderPreferences: function() { + var result; + + result = [ + React.DOM.li({'key': 'autoLockAfterMinutes'}, [ + React.DOM.p({'className': 'preferenceName'}, "Automatic lock (minutes) - m"), + React.DOM.p({'className': 'preferenceDescription'}, "Automatically lock Clipperz after N minutes. (0 = auto lock disabled)"), + this.renderPreferenceValueElement('lock.timeoutInMinutes') + ]), + React.DOM.li({'key': 'shouldShowDonationPanel'}, [ + React.DOM.p({'className': 'preferenceName'}, "Donation Panel - m"), + React.DOM.p({'className': 'preferenceDescription'}, "Select whether to display the donation panel or not"), + this.renderPreferenceValueElement('shouldShowDonationPanel') + ]) + ]; + + return result; + }, + + render: function () { + var result; + + if (! this.props.userInfo.preferences) { + result = React.DOM.p({}, "spinner..."); + } else { + result = React.DOM.div({'className':'extraFeature preferences'}, [ + React.DOM.div({'className':'header'}, [ + React.DOM.h1({}, "Preferences"), + React.DOM.div({'className':'description'}, [ + React.DOM.p({}, "Insert copy here..."), + ]) + ]), + React.DOM.div({'className':'content'}, [ + React.DOM.ul({'className':'preferenceList'}, this.renderPreferences()), + ]) + ]); + } + + return result; + }, + + //========================================================================= +}); + +Clipperz.PM.UI.Components.ExtraFeatures.Preferences = React.createFactory(Clipperz.PM.UI.Components.ExtraFeatures.PreferencesClass); diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/Pages/MainPage.js b/frontend/delta/js/Clipperz/PM/UI/Components/Pages/MainPage.js index b643830..8f15961 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/Pages/MainPage.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/Pages/MainPage.js @@ -42,6 +42,7 @@ Clipperz.PM.UI.Components.Pages.MainPageClass = React.createClass({ 'userInfo': React.PropTypes.object.isRequired, 'accountInfo': React.PropTypes.object.isRequired, 'style': React.PropTypes.oneOf(Clipperz_PM_UI_availableStyles).isRequired, + 'locked': React.PropTypes.bool, // 'mediaQueryStyle': React.PropTypes.oneOf(['extra-short', 'narrow', 'wide', 'extra-wide']).isRequired, // 'cards': React.PropTypes.deferred.isRequired }, @@ -55,17 +56,25 @@ Clipperz.PM.UI.Components.Pages.MainPageClass = React.createClass({ //========================================================================= render: function () { - var classes = { - 'mainPage': true - }; - classes[this.props['style']] = true; + var result; - return React.DOM.div({'key':'mainPage', 'className':Clipperz.PM.UI.Components.classNames(classes)}, [ - this.props['style'] != 'extra-wide' ? Clipperz.PM.UI.Components.Panels.SelectionPanel(this.props) : null, - Clipperz.PM.UI.Components.Panels.MainPanel(this.props), - Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanel(this.props), - this.props['ask'] ? Clipperz.PM.UI.Components.DialogBox(this.props['ask']) : null - ]); + if (this.props['locked']) { + result = null; + } else { + var classes = { + 'mainPage': true + }; + classes[this.props['style']] = true; + + result = React.DOM.div({'key':'mainPage', 'className':Clipperz.PM.UI.Components.classNames(classes)}, [ + this.props['style'] != 'extra-wide' ? Clipperz.PM.UI.Components.Panels.SelectionPanel(this.props) : null, + Clipperz.PM.UI.Components.Panels.MainPanel(this.props), + Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanel(this.props), + this.props['ask'] ? Clipperz.PM.UI.Components.DialogBox(this.props['ask']) : null + ]); + } + + return result; } //========================================================================= diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/Pages/UnlockPage.js b/frontend/delta/js/Clipperz/PM/UI/Components/Pages/UnlockPage.js new file mode 100644 index 0000000..5b98bd1 --- /dev/null +++ b/frontend/delta/js/Clipperz/PM/UI/Components/Pages/UnlockPage.js @@ -0,0 +1,174 @@ +/* + +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.Pages'); + +Clipperz.PM.UI.Components.Pages.UnlockPageClass = React.createClass({ + + propTypes: { + mode: React.PropTypes.oneOf(['CREDENTIALS','PIN']).isRequired, + // isNewUserRegistrationAvailable: React.PropTypes.bool.isRequired, + disabled: React.PropTypes.bool.isRequired + }, +/* + getDefaultProps: function () { + return { + mode: 'CREDENTIALS', + isNewUserRegistrationAvailable: false, + disabled: false, +// template: Clipperz.PM.UI.Components.PageTemplate + } + }, +*/ + getInitialState: function () { + return { + passphrase: '', + pin: '' + }; + }, + + //========================================================================= + + handleChange: function (anEvent) { + var newState = {}; + + newState['passphrase'] = anEvent.target.value; + + this.setState(newState); + }, + + // pollForChanges: function() { + // if (this.props.mode == 'CREDENTIALS') { + // var newState; + + // var usernameValue = this.refs['username'].getDOMNode().value; + // var passphraseValue = this.refs['passphrase'].getDOMNode().value; + + // newState = {}; + + // newState['username'] = (usernameValue) ? usernameValue : ""; + // newState['passphrase'] = (passphraseValue) ? passphraseValue : ""; + + // this.setState(newState); + // } + // }, + + //========================================================================= + + handlePassphraseSubmit: function (event) { + event.preventDefault(); + + this.refs['passphrase'].getDOMNode().blur(); + + MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'unlock', this.refs['passphrase'].getDOMNode().value); + + + }, + + resetUnlockForm: function() { + this.refs['passphrase'].getDOMNode().value = ''; + this.replaceState(this.getInitialState()); + }, + + //------------------------------------------------------------------------- + + shouldEnableUnlockButton: function () { + var result; + + return ( + (this.state['passphrase'] != '') + || + (this.state['pin'] != '') + ) + && + !this.props['disabled']; + }, + + unlockForm: function () { + return React.DOM.form({'key':'form', 'className':'unlockForm credentials', 'autoComplete':'off', 'autoCorrect':'off', 'autoCapitalize':'off', 'onChange':this.handleChange, 'onSubmit':this.handlePassphraseSubmit}, [ + React.DOM.div({'key':'fields'},[ + React.DOM.label({'key':'passphrase-label', 'htmlFor' :'passphrase'}, "passphrase"), + React.DOM.input({'key':'passphrase', 'type':'password', 'name':'passphrase', 'ref':'passphrase', 'placeholder':"passphrase"}) + ]), + React.DOM.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableUnlockButton(), 'className':'button'}, "unlock") + ]); + }, + + // handlePINSubmit: function (event) { + // event.preventDefault(); + + // this.refs['pin'].getDOMNode().blur(); + + // var credentials = { + // pin: this.refs['pin'].getDOMNode().value + // } + + // MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'doLogin', credentials); + // }, + + // pinForm: function () { + // return React.DOM.form({'className':'pinForm pin', 'autoComplete':'off', 'onChange':this.handleChange, 'onSubmit':this.handlePINSubmit}, [ + // React.DOM.div({'key':'pinFormDiv'},[ + // React.DOM.label({'for':'pin'}, "pin"), + // React.DOM.input({'type':'text', 'name':'pin', 'ref':'pin', placeholder:"PIN", 'key':'pin', 'autocapitalize':'none'}) + // ]), + // React.DOM.button({'key':'submitButton', 'type':'submit', 'disabled':this.props.disabled, 'className':'button'}, "login") + // ]); + // }, + + setInitialFocus: function () { + if (this.props.mode == 'PIN') { + this.refs['pin'].getDOMNode().select(); + } else { + this.refs['passphrase'].getDOMNode().select(); + } + }, + + // showUrl: function (anUrl) { + // return function () { + // window.open(anUrl, 'clipperz_about'); + // } + // }, + + render: function() { +//console.log("LOGIN PAGE", this.props); + return React.DOM.div({'key':'unlockForm', 'className':'unlockForm ' + this.props['style']}, [ + Clipperz.PM.UI.Components.AccountStatus(MochiKit.Base.update(this.props['proxyInfo'])), + React.DOM.header({'key':'header'}, [ + React.DOM.h3({}, 'Type your passphrase to unlock'), + ]), + React.DOM.div({'key':'formWrapper', 'className':'form'}, [ + this.props.mode == 'PIN' ? this.pinForm() : this.unlockForm(), + ]), + React.DOM.footer({'key':'footer'}, [ + React.DOM.div({'key':'applicationVersion', 'className':'applicationVersion'}, [ + React.DOM.span({'key':'applicationVersionLabel'}, "application version"), + React.DOM.a({'key':'applicationVersionLink', 'href':'https://github.com/clipperz/password-manager/commit/' + Clipperz_version, 'target':'github'}, Clipperz_version) + ]) + ]) + ]); + } +}); + +Clipperz.PM.UI.Components.Pages.UnlockPage = React.createFactory(Clipperz.PM.UI.Components.Pages.UnlockPageClass); 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 dd4a35f..b8b79bb 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/Panels/ExtraFeaturesPanel.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/Panels/ExtraFeaturesPanel.js @@ -81,7 +81,7 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({ }, lock: function () { -console.log("LOCK"); + MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'lock'); }, logout: function () { @@ -121,6 +121,9 @@ console.log("LOCK"); if (aComponentName == 'OTP') { MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'updateOTPListAndDetails'); } + if (aComponentName == 'Preferences') { + MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'updatePreferences'); + } this.setState({ 'isFullyOpen':true, @@ -150,6 +153,9 @@ console.log("LOCK"); 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_preferences', 'onClick':this.toggleExtraFeatureComponent('Preferences'), 'className':(this.state['extraFeatureComponentName'] == 'Preferences') ? 'selected' : ''}, [ + React.DOM.h2({'key':'account_preferences_h2'}, "Preferences"), + ]), 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'}, [ @@ -280,7 +286,7 @@ console.log("LOCK"); ]) ]), React.DOM.li({'key':'logout', 'className':'lock-logout'}, [ -// React.DOM.h2({'className':'lock', 'onClick':this.lock}, "Lock"), + React.DOM.h2({'className':'lock', 'onClick':this.lock}, "Lock"), React.DOM.h2({'className':'logout', 'onClick':this.logout}, "Logout"), ]) ]) diff --git a/frontend/delta/js/Clipperz/PM/UI/MainController.js b/frontend/delta/js/Clipperz/PM/UI/MainController.js index 81e6b75..bdce918 100644 --- a/frontend/delta/js/Clipperz/PM/UI/MainController.js +++ b/frontend/delta/js/Clipperz/PM/UI/MainController.js @@ -51,10 +51,14 @@ Clipperz.PM.UI.MainController = function() { this._closeMaskAction = null; + this._lockListener = null; + this._lockTimeout = null; + this._pages = {}; this.renderPages([ 'loginPage', 'registrationPage', + 'unlockPage', 'mainPage', 'cardDetailPage', 'errorPage', @@ -62,10 +66,12 @@ Clipperz.PM.UI.MainController = function() { this.registerForNotificationCenterEvents([ 'doLogin', 'registerNewUser', 'showRegistrationForm', 'goBack', - 'logout', + 'logout', 'unlock', + 'enableLock', 'disableLock', +// 'lock', 'changePassphrase', 'deleteAccount', + 'updatePreferences', 'setPreference', 'updateOTPListAndDetails', 'createNewOTP', 'deleteOTPs', 'changeOTPLabel', -// 'export', 'importCards', 'downloadExport', 'updateProgress', @@ -170,7 +176,7 @@ MochiKit.Base.update(Clipperz.PM.UI.MainController.prototype, { //========================================================================= showOfflineError: function () { -console.log("THE BROWSER IS OFFLINE"); +Clipperz.log("THE BROWSER IS OFFLINE"); }, selectInitialProxy: function () { @@ -212,6 +218,7 @@ console.log("THE BROWSER IS OFFLINE"); MochiKit.Iter.forEach(events, function (anEvent) { MochiKit.Signal.connect(Clipperz.Signal.NotificationCenter, anEvent, MochiKit.Base.method(self, anEvent + '_handler')); + MochiKit.Signal.connect(Clipperz.Signal.NotificationCenter, anEvent, MochiKit.Base.method(self, 'resetLockTimeout')); }); // MochiKit.Signal.connect(window, 'onpopstate', MochiKit.Base.method(this, 'historyGoBack')); @@ -383,6 +390,75 @@ console.log("THE BROWSER IS OFFLINE"); return deferredResult; }, + lock: function () { + var deferredResult; + + MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'disableLock'); + + deferredResult = new Clipperz.Async.Deferred('MainController.lock_handler', {trace:false}); + // force exit edit state OR skip lock if pending changes are present + deferredResult.addMethod(this, 'exitCurrentSelection'); + deferredResult.addMethod(this, 'resetPanels'); + deferredResult.addMethod(this, 'moveOutPage', this.currentPage(), 'unlockPage'); + deferredResult.addMethod(this.user(), 'lock'); + deferredResult.addMethod(this, 'deleteCleanTextData'); + deferredResult.addMethod(this.pages()['unlockPage'], 'setInitialFocus'); + deferredResult.addCallback(MochiKit.Async.callLater, 1, MochiKit.Base.method(this.pages()['mainPage'], 'replaceProps', {'locked': true})); + + deferredResult.callback(); + + return deferredResult; + }, + + unlock_handler: function(aPassphrase) { + var deferredResult; + + var user = this.user(); + var unlockPage = this.pages()['unlockPage']; + + deferredResult = new Clipperz.Async.Deferred('MainController.unlock_handler', {trace:false}); + deferredResult.addMethod(unlockPage, 'setProps', {'disabled': true}); + + deferredResult.addMethod(user, 'setPassphraseFunction', function() { return MochiKit.Async.succeed(aPassphrase); }); + deferredResult.addMethod(user, 'getPreferences'); // #ASK -> user.unlock +// deferredResult.addMethod(user, 'unlock', function() { return MochiKit.Async.succeed(aPassphrase); }); + deferredResult.addErrback(function (aValue) { + var innerDeferredResult; + + innerDeferredResult = new Clipperz.Async.Deferred('MainController.unlock_handler ', {trace:false}); + innerDeferredResult.addMethod(user, 'getHeaderIndex', 'preferences'); + innerDeferredResult.addMethodcaller('deleteAllCleanTextData'); + innerDeferredResult.addMethod(unlockPage, 'setProps', {'disabled': false}); + innerDeferredResult.addMethod(unlockPage, 'setInitialFocus'); + innerDeferredResult.addCallback(MochiKit.Async.fail, aValue); + innerDeferredResult.callback(); + + return aValue; + }); + + deferredResult.addMethod(this, 'moveInPage', this.currentPage(), 'mainPage'); // #ASK or cardDetailPage + deferredResult.addMethod(this, 'refreshUI'); + deferredResult.addMethod(unlockPage, 'setProps', {'disabled': false}); // #ASK ? + deferredResult.addMethod(unlockPage, 'resetUnlockForm'); // #ASK ? +// deferredResult.addMethod(this, 'resetLockTimeout'); + deferredResult.addCallback(MochiKit.Signal.signal, Clipperz.Signal.NotificationCenter, 'enableLock'); + + deferredResult.callback(); + + return deferredResult; + + // this.user().setPassphraseFunction(function(){return aPassphrase;}); +// TODO: check if passphrase is correct by try/catch on decrypting something + // this.moveOutPage(this.currentPage(), 'mainPage'); +// TODO: check why the unlock form keeps the value stored (doesn't happen with the login form...) + }, + + deleteCleanTextData: function() { + this._recordsInfo = null; + this._selectedCards = null; + this._selectedCardInfo = null; + }, + //------------------------------------------------------------------------- registerNewUser_handler: function (credentials) { @@ -489,6 +565,7 @@ console.log("THE BROWSER IS OFFLINE"); collectRecordInfo: function (aRecord) { var deferredResult; +//console.log("collectRecordInfo"); deferredResult = new Clipperz.Async.Deferred('MainController.collectRecordInfo', {trace:false}); deferredResult.setValue('_record'); deferredResult.addMethod(aRecord, 'reference'); @@ -551,7 +628,6 @@ console.log("THE BROWSER IS OFFLINE"); // deferredResult.addMethod(this, function(d) {console.log(d); return d;}); deferredResult.addMethod(this, 'collectRecordInfo'); - deferredResult.addMethod(this, 'setPageProperties', 'mainPage', 'selectedCard'); if ((this.mediaQueryStyle() == 'narrow') && shouldShowCardDetail) { deferredResult.addMethod(this, 'showCardDetailInNarrowView'); @@ -724,9 +800,46 @@ console.log("THE BROWSER IS OFFLINE"); } deferredResult.callback(); - + return deferredResult; }, + + //---------------------------------------------------------------------------- + + enableLock_handler: function () { + MochiKit.Signal.connect(Clipperz.Signal.NotificationCenter, 'lock', MochiKit.Base.method(this, 'lock')); + }, + + disableLock_handler: function () { + MochiKit.Signal.disconnectAll(Clipperz.Signal.NotificationCenter, 'lock'); + }, + +// clearLockTimeout: function () { +// clearTimeout(this._lockTimeout); +// }, + + resetLockTimeout: function () { + if (this.user()) { + return Clipperz.Async.callbacks("MainController.resetLockTimeout", [ + MochiKit.Base.method(this.user(), 'getPreference', 'lock.timeoutInMinutes'), + MochiKit.Base.bind( function (aDelay) { + var aDelay = aDelay * 60*1000; + + if (this._lockTimeout) { +// console.log("clearing previous lock timer"); + clearTimeout(this._lockTimeout); + } + + if (aDelay > 0) { +// console.log("setting lock timer", aDelay); + this._lockTimeout = setTimeout(function() { + MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'lock'); + }, aDelay); + } + }, this) + ], {trace:false}); + } + }, //---------------------------------------------------------------------------- @@ -776,7 +889,7 @@ console.log("THE BROWSER IS OFFLINE"); var props = {}; props[aKey] = aValue; this.pages()[aPageName].setProps(props); - + return aValue; }, @@ -813,6 +926,9 @@ console.log("THE BROWSER IS OFFLINE"); runApplication: function (anUser) { this.moveInPage(this.currentPage(), 'mainPage'); + MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'enableLock'); + this.resetLockTimeout(); + return this.renderAccountData(); }, @@ -1086,6 +1202,7 @@ console.log("THE BROWSER IS OFFLINE"); 'featureSet': this.featureSet(), 'features': this.features(), 'proxyInfo': this.proxyInfo(), + 'locked': false // 'shouldIncludeArchivedCards': this.shouldIncludeArchivedCards(), // 'cards': …, // 'tags': …, @@ -1302,6 +1419,7 @@ console.log("THE BROWSER IS OFFLINE"); }, refreshCardEditDetail_handler: function (aRecordReference) { +//console.log("refreshCardEditDetail_handler"); this.updateSelectedCard({'reference':aRecordReference}, false, true); }, @@ -1371,6 +1489,36 @@ console.log("THE BROWSER IS OFFLINE"); return deferredResult; }, + updatePreferences: function() { + return Clipperz.Async.callbacks("MainController.updatePreferences_handler", [ + MochiKit.Base.method(this.user(), 'getPreferences'), + MochiKit.Base.bind(function(somePreferences) { + return MochiKit.Base.update(this.userInfo(), {'preferences': somePreferences}) + }, this), + MochiKit.Base.bind(function(someUserInfo) { + this.pages()['mainPage'].setProps({ + 'userInfo': someUserInfo + }) + }, this), + ], {trace:false}); + }, + + updatePreferences_handler: function() { + this.updatePreferences(); + }, + + setPreference_handler: function(aKey, aValue) { + // return this.user().setPreference(aKey, aValue); + + return Clipperz.Async.callbacks("MainController.setPreference_handler", [ + MochiKit.Base.method(this.overlay(), 'show', "", true), + MochiKit.Base.method(this.user(), 'setPreference', aKey, aValue), + MochiKit.Base.method(this, 'refreshCurrentPage'), + MochiKit.Base.method(this, 'updatePreferences'), + MochiKit.Base.method(this.overlay(), 'done', "", 0.5), + ], {trace:false}); + }, + importCards_handler: function(data) { return Clipperz.Async.callbacks("MainController.importCards_handler", [ MochiKit.Base.method(this.overlay(), 'show', "importing …", true), @@ -1407,7 +1555,7 @@ console.log("THE BROWSER IS OFFLINE"); updateOTPListAndDetails: function() { return Clipperz.Async.callbacks("MainController.updateOTPListAndDetails", [ - Clipperz.Async.collectResults("User.updateOTPListAndDetails ", { + Clipperz.Async.collectResults("MainController.updateOTPListAndDetails ", { 'userInfo': MochiKit.Base.method(this, 'userInfo'), 'otpDetails': Clipperz.Async.collectResults("User.updateOTPListAndDetails ", { 'otpList': MochiKit.Base.method(this.user(),'getOneTimePasswords'), @@ -1478,6 +1626,7 @@ console.log("THE BROWSER IS OFFLINE"); MochiKit.Base.method(this, 'saveChanges'), MochiKit.Base.method(currentPage, 'setProps', {'mode':'view', 'showGlobalMask':false}), MochiKit.Base.method(this, 'refreshUI', aRecordReference), + MochiKit.Base.partial(MochiKit.Signal.signal, Clipperz.Signal.NotificationCenter, 'enableLock'), ], {trace:false}); }, @@ -1523,6 +1672,7 @@ console.log("THE BROWSER IS OFFLINE"); this.goBackToMainPage(); } },this), + MochiKit.Base.partial(MochiKit.Signal.signal, Clipperz.Signal.NotificationCenter, 'enableLock'), ], {trace:false}); }, @@ -1637,7 +1787,8 @@ console.log("THE BROWSER IS OFFLINE"); var currentPage = this.pages()[this.currentPage()]; currentPage.setProps({'mode': 'edit'}); - + MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'disableLock'); + return Clipperz.Async.callbacks("MainController.enterEditMode", [ MochiKit.Base.method(this, 'allTags', true), MochiKit.Base.keys, diff --git a/frontend/delta/properties/delta.properties.json b/frontend/delta/properties/delta.properties.json index da55369..7b6e5ab 100644 --- a/frontend/delta/properties/delta.properties.json +++ b/frontend/delta/properties/delta.properties.json @@ -178,6 +178,7 @@ "Clipperz/PM/UI/Components/Pages/LoginPage.js", "Clipperz/PM/UI/Components/Pages/RegistrationPage.js", + "Clipperz/PM/UI/Components/Pages/UnlockPage.js", "Clipperz/PM/UI/Components/Pages/MainPage.js", "Clipperz/PM/UI/Components/Pages/CardDetailPage.js", "Clipperz/PM/UI/Components/Pages/ErrorPage.js", @@ -187,6 +188,7 @@ "Clipperz/PM/UI/Components/Panels/ExtraFeaturesPanel.js", "Clipperz/PM/UI/Components/ExtraFeatures/DevicePIN.js", + "Clipperz/PM/UI/Components/ExtraFeatures/Preferences.js", "Clipperz/PM/UI/Components/ExtraFeatures/Passphrase.js", "Clipperz/PM/UI/Components/ExtraFeatures/OTP.js", "Clipperz/PM/UI/Components/ExtraFeatures/DeleteAccount.js",