1148 lines
39 KiB
JavaScript
1148 lines
39 KiB
JavaScript
/*
|
|
|
|
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.DataModel');
|
|
|
|
//if (typeof(Clipperz) == 'undefined') { Clipperz = {}; }
|
|
//if (typeof(Clipperz.PM) == 'undefined') { Clipperz.PM = {}; }
|
|
//if (typeof(Clipperz.PM.DataModel) == 'undefined') { Clipperz.PM.DataModel = {}; }
|
|
|
|
|
|
//#############################################################################
|
|
|
|
Clipperz.PM.DataModel.User = function (args) {
|
|
args = args || {};
|
|
|
|
Clipperz.PM.DataModel.User.superclass.constructor.apply(this, arguments);
|
|
|
|
this._username = args.username || null;
|
|
this._getPassphraseFunction = args.getPassphraseFunction || null;
|
|
|
|
this._data = null;
|
|
|
|
this._connection = null;
|
|
this._connectionVersion = 'current';
|
|
|
|
this._accountInfo = null;
|
|
this._serverData = null;
|
|
// this._serverLockValue = null;
|
|
this._transientState = null;
|
|
|
|
this._deferredLocks = {
|
|
'passphrase': new MochiKit.Async.DeferredLock(),
|
|
'serverData': new MochiKit.Async.DeferredLock(),
|
|
// 'recordsIndex': new MochiKit.Async.DeferredLock(),
|
|
// 'directLoginsIndex': new MochiKit.Async.DeferredLock()
|
|
// 'preferences': new MochiKit.Async.DeferredLock()
|
|
// 'oneTimePasswords': new MochiKit.Async.DeferredLock()
|
|
'__syntaxFix__': 'syntax fix'
|
|
};
|
|
|
|
this._usedOTP = null;
|
|
|
|
return this;
|
|
}
|
|
|
|
Clipperz.Base.extend(Clipperz.PM.DataModel.User, Object, {
|
|
|
|
'toString': function () {
|
|
return "Clipperz.PM.DataModel.User - " + this.username();
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'username': function () {
|
|
return this._username;
|
|
},
|
|
|
|
'setUsername': function (aValue) {
|
|
this._username = aValue;
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'setUsedOTP': function(aOTP) {
|
|
this._usedOTP = aOTP;
|
|
|
|
return aOTP;
|
|
},
|
|
|
|
'resetUsedOTP': function(aOTP) {
|
|
this._usedOTP = null;
|
|
},
|
|
|
|
'markUsedOTP': function(aOTP) {
|
|
var result;
|
|
var oneTimePasswordKey;
|
|
|
|
if (this._usedOTP) {
|
|
oneTimePasswordKey = Clipperz.PM.DataModel.OneTimePassword.computeKeyWithPassword(
|
|
Clipperz.PM.DataModel.OneTimePassword.normalizedOneTimePassword(this._usedOTP)
|
|
);
|
|
|
|
result = Clipperz.Async.callbacks("User.markUsedOTP", [ // NOTE: fired also when passphrase looks exactly like OTP
|
|
MochiKit.Base.method(this, 'getHeaderIndex', 'oneTimePasswords'),
|
|
MochiKit.Base.methodcaller('markOTPAsUsed', oneTimePasswordKey),
|
|
MochiKit.Base.method(this,'saveChanges'), // Too 'heavy'?
|
|
MochiKit.Base.method(this, 'resetUsedOTP')
|
|
], {'trace': false});
|
|
} else {
|
|
result = MochiKit.Async.succeed();
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
// this.setSubscription(new Clipperz.PM.DataModel.User.Subscription(someServerData['subscription']));
|
|
'accountInfo': function () {
|
|
return this._accountInfo;
|
|
},
|
|
|
|
'setAccountInfo': function (aValue) {
|
|
this._accountInfo = aValue;
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'displayName': function() {
|
|
return "" + this.username() + "";
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'data': function () {
|
|
if (this._data == null) {
|
|
this._data = new Clipperz.KeyValueObjectStore(/*{'name':'User.data [1]'}*/);
|
|
};
|
|
|
|
return this._data;
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
/*
|
|
'serverLockValue': function () {
|
|
return this._serverLockValue;
|
|
},
|
|
|
|
'setServerLockValue': function (aValue) {
|
|
this._serverLockValue = aValue;
|
|
},
|
|
*/
|
|
//-------------------------------------------------------------------------
|
|
|
|
'transientState': function () {
|
|
if (this._transientState == null) {
|
|
this._transientState = {}
|
|
}
|
|
|
|
return this._transientState;
|
|
},
|
|
|
|
'resetTransientState': function (isCommitting) {
|
|
this._transientState = null;
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'deferredLockForSection': function(aSectionName) {
|
|
return this._deferredLocks[aSectionName];
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'getPassphrase': function() {
|
|
return this._getPassphraseFunction();
|
|
},
|
|
|
|
'getPassphraseFunction': function () {
|
|
return this._getPassphraseFunction;
|
|
},
|
|
|
|
'setPassphraseFunction': function (aFunction) {
|
|
this._getPassphraseFunction = aFunction;
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'getCredentials': function () {
|
|
return Clipperz.Async.collectResults("User; get username and passphrase", {
|
|
'username': MochiKit.Base.method(this, 'username'),
|
|
'password': MochiKit.Base.method(this, 'getPassphrase')
|
|
}, {trace:false})();
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'changePassphrase': function (aNewValueCallback) {
|
|
return this.updateCredentials(this.username(), aNewValueCallback);
|
|
},
|
|
|
|
//.........................................................................
|
|
|
|
'updateCredentials': function (aUsername, aPassphraseCallback) {
|
|
var deferredResult;
|
|
|
|
deferredResult = new Clipperz.Async.Deferred("User.updateCredentials", {trace:false});
|
|
deferredResult.addMethod(this.connection(), 'ping');
|
|
deferredResult.collectResults({
|
|
'newUsername': MochiKit.Base.partial(MochiKit.Async.succeed, aUsername),
|
|
'newPassphrase': aPassphraseCallback,
|
|
'user': MochiKit.Base.method(this, 'prepareRemoteDataWithKeyFunction', aPassphraseCallback),
|
|
'oneTimePasswords': [
|
|
MochiKit.Base.method(this, 'getHeaderIndex', 'oneTimePasswords'),
|
|
MochiKit.Base.methodcaller('getEncryptedOTPData', aPassphraseCallback),
|
|
function (otps) {
|
|
var result;
|
|
var otpRefs;
|
|
var i, c;
|
|
|
|
result = {};
|
|
otpRefs = MochiKit.Base.keys(otps);
|
|
c = otpRefs.length;
|
|
for (i=0; i<c; i++) {
|
|
result[otpRefs[i]] = {}
|
|
result[otpRefs[i]]['data'] = otps[otpRefs[i]]['data'];
|
|
result[otpRefs[i]]['version'] = otps[otpRefs[i]]['version'];
|
|
}
|
|
|
|
return result;
|
|
}
|
|
]
|
|
});
|
|
|
|
deferredResult.addMethod(this.connection(), 'updateCredentials');
|
|
deferredResult.callback();
|
|
|
|
return deferredResult;
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'initialSetupWithNoData': function () {
|
|
this._serverData = {
|
|
'version': '0.1',
|
|
'statistics': "",
|
|
'header': {
|
|
'data': null,
|
|
'version': Clipperz.PM.Crypto.encryptingFunctions.currentVersion,
|
|
|
|
'recordsIndex': new Clipperz.PM.DataModel.User.Header.RecordIndex({
|
|
'retrieveKeyFunction': MochiKit.Base.method(this, 'getPassphrase'),
|
|
'recordsData': {'data':null, 'index':{}},
|
|
'recordsStats': null,
|
|
'directLoginsData': {'data':null, 'index':{}},
|
|
'attachmentsData': {'data':null, 'index':{}},
|
|
'encryptedDataVersion': Clipperz.PM.Crypto.encryptingFunctions.currentVersion,
|
|
'retrieveRecordDetailFunction': MochiKit.Base.method(this, 'getRecordDetail')
|
|
}),
|
|
'preferences': new Clipperz.PM.DataModel.User.Header.Preferences({
|
|
'name': 'preferences',
|
|
'retrieveKeyFunction': MochiKit.Base.method(this, 'getPassphrase')
|
|
}),
|
|
'oneTimePasswords': new Clipperz.PM.DataModel.User.Header.OneTimePasswords({
|
|
'connection': this.connection(),
|
|
'name': 'oneTimePasswords',
|
|
'username': this.username(),
|
|
'passphraseCallback': MochiKit.Base.method(this, 'getPassphrase')
|
|
})
|
|
}
|
|
};
|
|
|
|
// this._serverLockValue = Clipperz.PM.Crypto.randomKey();
|
|
},
|
|
|
|
//.........................................................................
|
|
|
|
'registerAsNewAccount': function () {
|
|
var deferredResult;
|
|
|
|
deferredResult = new Clipperz.Async.Deferred("User.registerAsNewAccount", {trace:false});
|
|
deferredResult.addMethod(this, 'initialSetupWithNoData')
|
|
deferredResult.addMethod(this, 'getPassphrase');
|
|
deferredResult.addMethod(this, 'prepareRemoteDataWithKey');
|
|
deferredResult.addMethod(this.connection(), 'register');
|
|
deferredResult.callback();
|
|
|
|
return deferredResult;
|
|
},
|
|
|
|
'deleteAccount': function() {
|
|
var deferredResult;
|
|
|
|
deferredResult = new MochiKit.Async.Deferred("User.deleteAccount", {trace:false});
|
|
deferredResult.addCallback(MochiKit.Base.method(this.connection(), 'message'), 'deleteUser');
|
|
deferredResult.addCallback(MochiKit.Base.method(this, 'resetAllLocalData'));
|
|
deferredResult.callback();
|
|
|
|
return deferredResult;
|
|
},
|
|
|
|
'resetAllLocalData': function() {
|
|
var deferredResult;
|
|
|
|
deferredResult = new MochiKit.Async.Deferred("User.resetAllLocalData", {trace:false});
|
|
deferredResult.addCallback(MochiKit.Base.method(this, 'deleteAllCleanTextData'));
|
|
deferredResult.addCallback(MochiKit.Base.method(this, function() {
|
|
this.resetConnection();
|
|
this.setUsername("");
|
|
this._getPassphraseFunction = function() { return ""; };
|
|
this._serverData = null;
|
|
}));
|
|
|
|
deferredResult.callback();
|
|
|
|
return deferredResult;
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'login': function () {
|
|
var deferredResult;
|
|
var oneTimePasswordReference;
|
|
|
|
deferredResult = new Clipperz.Async.Deferred("User.login", {trace:false});
|
|
deferredResult.addMethod(this, 'getPassphrase');
|
|
deferredResult.addCallback(Clipperz.PM.DataModel.OneTimePassword.isValidOneTimePasswordValue);
|
|
|
|
deferredResult.addCallback(Clipperz.Async.deferredIf("Is the passphrase an OTP", [
|
|
MochiKit.Base.method(this,'getPassphrase'),
|
|
MochiKit.Base.method(this,'setUsedOTP'),
|
|
MochiKit.Base.method(this, 'getCredentials'),
|
|
MochiKit.Base.method(this.connection(), 'redeemOneTimePassword'),
|
|
function (aPassphrase) {
|
|
return MochiKit.Base.partial(MochiKit.Async.succeed, aPassphrase);
|
|
},
|
|
MochiKit.Base.method(this, 'setPassphraseFunction')
|
|
], []));
|
|
|
|
deferredResult.addBoth(MochiKit.Base.method(this, 'loginWithPassphrase'));
|
|
deferredResult.addBothPass(MochiKit.Base.method(this, 'resetUsedOTP'));
|
|
|
|
deferredResult.callback();
|
|
|
|
return deferredResult;
|
|
},
|
|
|
|
'loginWithPassphrase': function () {
|
|
var deferredResult;
|
|
|
|
deferredResult = new Clipperz.Async.Deferred("User.loginWithPassphrase", {trace:false});
|
|
|
|
deferredResult.addMethod(this, 'getPassphrase');
|
|
deferredResult.addMethod(this.connection(), 'login', false);
|
|
deferredResult.addMethod(this, 'setupAccountInfo');
|
|
deferredResult.addMethod(this, 'markUsedOTP');
|
|
deferredResult.addErrback (MochiKit.Base.method(this, 'handleConnectionFallback'));
|
|
|
|
deferredResult.callback();
|
|
|
|
return deferredResult;
|
|
},
|
|
|
|
//.........................................................................
|
|
|
|
'handleConnectionFallback': function(aValue) {
|
|
var result;
|
|
|
|
if (aValue instanceof MochiKit.Async.CancelledError) {
|
|
result = aValue;
|
|
} else if ((aValue['isPermanent'] === true) || (Clipperz.PM.Connection.communicationProtocol.fallbackVersions[this.connectionVersion()] == null)) {
|
|
result = Clipperz.Async.callbacks("User.handleConnectionFallback - failed", [
|
|
MochiKit.Base.method(this.data(), 'removeValue', 'passphrase'),
|
|
MochiKit.Base.method(this, 'setConnectionVersion', 'current'),
|
|
MochiKit.Base.partial(MochiKit.Async.fail, aValue)
|
|
], {trace:false});
|
|
} else {
|
|
this.setConnectionVersion(Clipperz.PM.Connection.communicationProtocol.fallbackVersions[this.connectionVersion()]);
|
|
result = new Clipperz.Async.Deferred("User.handleConnectionFallback - retry");
|
|
result.addMethod(this, 'loginWithPassphrase');
|
|
result.callback();
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'setupAccountInfo': function (aValue) {
|
|
this.setAccountInfo(new Clipperz.PM.DataModel.User.AccountInfo(aValue['accountInfo']));
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'lock': function () {
|
|
return Clipperz.Async.callbacks("User.lock", [
|
|
MochiKit.Base.method(this.connection(), 'logout'),
|
|
MochiKit.Base.method(this, 'deleteAllCleanTextData'),
|
|
MochiKit.Base.method(this, 'setPassphraseFunction', function() {throw("No passphrase set.")})
|
|
], {trace:false});
|
|
},
|
|
|
|
'unlock': function (aPassphraseDelegate) {
|
|
this.setPassphraseFunction(aPassphraseDelegate);
|
|
return this.getPreferences(); // TODO: make this more explicit.
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'logout': function () {
|
|
return Clipperz.Async.callbacks("User.logout", [
|
|
MochiKit.Base.method(this, 'deleteAllCleanTextData'),
|
|
MochiKit.Base.method(this.connection(), 'logout')
|
|
], {trace:false});
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'headerFormatVersion': function(anHeader) {
|
|
var result;
|
|
|
|
if (anHeader.charAt(0) == '{') {
|
|
var headerData;
|
|
|
|
headerData = Clipperz.Base.evalJSON(anHeader);
|
|
result = headerData['version'];
|
|
} else {
|
|
result = 'LEGACY';
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'unpackServerData': function (someServerData) {
|
|
var unpackedData;
|
|
var headerVersion;
|
|
|
|
var recordsIndex;
|
|
var preferences;
|
|
var oneTimePasswords;
|
|
|
|
headerVersion = this.headerFormatVersion(someServerData['header']);
|
|
switch (headerVersion) {
|
|
case 'LEGACY':
|
|
var legacyHeader;
|
|
|
|
legacyHeader = new Clipperz.PM.DataModel.User.Header.Legacy({
|
|
'retrieveKeyFunction': MochiKit.Base.method(this, 'getPassphrase'),
|
|
'remoteData': {
|
|
'data': someServerData['header'],
|
|
'version': someServerData['version'],
|
|
'recordsStats': someServerData['recordsStats']
|
|
},
|
|
// 'encryptedDataKeypath': 'data',
|
|
// 'encryptedVersionKeypath': 'version',
|
|
'retrieveRecordDetailFunction': MochiKit.Base.method(this, 'getRecordDetail')
|
|
});
|
|
|
|
recordsIndex = legacyHeader;
|
|
preferences = legacyHeader;
|
|
oneTimePasswords = legacyHeader;
|
|
break;
|
|
case '0.1':
|
|
var headerData;
|
|
|
|
//console.log("Server data", someServerData);
|
|
headerData = Clipperz.Base.evalJSON(someServerData['header']);
|
|
|
|
//console.log("headerData", headerData);
|
|
recordsIndex = new Clipperz.PM.DataModel.User.Header.RecordIndex({
|
|
'retrieveKeyFunction': MochiKit.Base.method(this, 'getPassphrase'),
|
|
'recordsData': headerData['records'],
|
|
'recordsStats': someServerData['recordsStats'],
|
|
'directLoginsData': headerData['directLogins'],
|
|
'attachmentsData': headerData['attachments'] || {'data': null, 'index':{}},
|
|
'encryptedDataVersion': someServerData['version'],
|
|
'retrieveRecordDetailFunction': MochiKit.Base.method(this, 'getRecordDetail')
|
|
});
|
|
|
|
// Still missing a test case that actually fais with the old version of the code, where the check for undefined was missing
|
|
if (typeof(headerData['preferences']) != 'undefined') {
|
|
preferences = new Clipperz.PM.DataModel.User.Header.Preferences({
|
|
'name': 'preferences',
|
|
'retrieveKeyFunction': MochiKit.Base.method(this, 'getPassphrase'),
|
|
'remoteData': {
|
|
'data': headerData['preferences']['data'],
|
|
'version': someServerData['version']
|
|
}
|
|
});
|
|
} else {
|
|
preferences = new Clipperz.PM.DataModel.User.Header.Preferences({
|
|
'name': 'preferences',
|
|
'retrieveKeyFunction': MochiKit.Base.method(this, 'getPassphrase')
|
|
});
|
|
}
|
|
|
|
if (typeof(headerData['oneTimePasswords']) != 'undefined') {
|
|
oneTimePasswords = new Clipperz.PM.DataModel.User.Header.OneTimePasswords({
|
|
'name': 'oneTimePasswords',
|
|
'connection': this.connection(),
|
|
'username': this.username(),
|
|
'retrieveKeyFunction': MochiKit.Base.method(this, 'getPassphrase'),
|
|
'remoteData': {
|
|
'data': headerData['oneTimePasswords']['data'],
|
|
'version': someServerData['version']
|
|
}
|
|
});
|
|
} else {
|
|
oneTimePasswords = new Clipperz.PM.DataModel.User.Header.OneTimePasswords({
|
|
'name': 'OneTimePasswords',
|
|
'connection': this.connection(),
|
|
'username': this.username(),
|
|
'retrieveKeyFunction': MochiKit.Base.method(this, 'getPassphrase')
|
|
});
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
unpackedData = {
|
|
'version': someServerData['version'],
|
|
'statistics': someServerData['statistics'],
|
|
'header': {
|
|
'data': someServerData['header'],
|
|
'version': headerVersion,
|
|
|
|
'recordsIndex': recordsIndex,
|
|
'preferences': preferences,
|
|
'oneTimePasswords': oneTimePasswords
|
|
}
|
|
};
|
|
|
|
this._serverData = unpackedData;
|
|
|
|
return this._serverData;
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'getServerData': function() {
|
|
var deferredResult;
|
|
|
|
deferredResult = new Clipperz.Async.Deferred("User.getServerData", {trace:false});
|
|
deferredResult.acquireLock(this.deferredLockForSection('serverData'));
|
|
deferredResult.addCallback(MochiKit.Base.bind(function(aResult) {
|
|
var innerDeferredResult;
|
|
|
|
innerDeferredResult = new Clipperz.Async.Deferred("User.getUserDetails.innerDeferred", {trace:false});
|
|
if (this._serverData == null) {
|
|
innerDeferredResult.addCallbackPass(MochiKit.Signal.signal, this, 'loadingUserDetails');
|
|
innerDeferredResult.addMethod(this.connection(), 'message', 'getUserDetails');
|
|
innerDeferredResult.addMethod(this, 'unpackServerData');
|
|
innerDeferredResult.addCallbackPass(MochiKit.Signal.signal, this, 'loadedUserDetails');
|
|
}
|
|
|
|
innerDeferredResult.addCallback(MochiKit.Base.bind(function () {
|
|
return this._serverData;
|
|
},this));
|
|
innerDeferredResult.callback();
|
|
|
|
return innerDeferredResult;
|
|
}, this));
|
|
deferredResult.releaseLock(this.deferredLockForSection('serverData'));
|
|
deferredResult.callback();
|
|
|
|
return deferredResult;
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'connectionVersion': function() {
|
|
return this._connectionVersion;
|
|
},
|
|
|
|
'setConnectionVersion': function(aValue) {
|
|
if (this._connectionVersion != aValue) {
|
|
this.resetConnection();
|
|
}
|
|
this._connectionVersion = aValue;
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'connection': function() {
|
|
if ((this._connection == null) && (this.connectionVersion() != null) ){
|
|
this._connection = new Clipperz.PM.Connection.communicationProtocol.versions[this.connectionVersion()]({
|
|
getCredentialsFunction: MochiKit.Base.method(this, 'getCredentials')
|
|
});
|
|
}
|
|
|
|
return this._connection;
|
|
},
|
|
|
|
'resetConnection': function(aValue) {
|
|
if (this._connection != null) {
|
|
this._connection.reset();
|
|
}
|
|
|
|
this._connection = null;
|
|
},
|
|
|
|
//=========================================================================
|
|
|
|
'getHeaderIndex': function (aKey) {
|
|
return Clipperz.Async.callbacks("User.getHeaderIndex", [
|
|
MochiKit.Base.method(this, 'getServerData'),
|
|
MochiKit.Base.itemgetter('header'),
|
|
MochiKit.Base.itemgetter(aKey)
|
|
], {trace:false})
|
|
},
|
|
|
|
//=========================================================================
|
|
|
|
'getTags': function (shouldIncludeArchivedCards) {
|
|
var archivedCardsFilter = (shouldIncludeArchivedCards || false)
|
|
? MochiKit.Async.succeed
|
|
: MochiKit.Base.partial(MochiKit.Base.filter, function (someTags) {
|
|
return someTags.indexOf(Clipperz.PM.DataModel.Record.archivedTag) == -1;
|
|
});
|
|
|
|
return Clipperz.Async.callbacks("User.getTags", [
|
|
MochiKit.Base.method(this, 'getRecords'),
|
|
MochiKit.Base.partial(MochiKit.Base.map, MochiKit.Base.methodcaller('tags')),
|
|
Clipperz.Async.collectAll,
|
|
archivedCardsFilter,
|
|
MochiKit.Base.flattenArray,
|
|
MochiKit.Iter.groupby,
|
|
function (someGroups) {
|
|
return MochiKit.Iter.reduce(function(aCollector, aGroup) {
|
|
var currentValue = aCollector[aGroup[0]] ? aCollector[aGroup[0]] : 0;
|
|
aCollector[aGroup[0]] = MochiKit.Iter.list(aGroup[1]).length + currentValue;
|
|
|
|
return aCollector;
|
|
}, someGroups, {});
|
|
}
|
|
], {trace:false});
|
|
},
|
|
|
|
//=========================================================================
|
|
|
|
'recordWithData': function (recordData) {
|
|
//console.log("recordWithData", recordData)
|
|
return Clipperz.Async.callbacks("User.recordWithData", [
|
|
MochiKit.Base.method(this, 'getHeaderIndex', 'recordsIndex'),
|
|
MochiKit.Base.methodcaller('records'),
|
|
MochiKit.Base.itemgetter(recordData['reference']),
|
|
MochiKit.Base.methodcaller('setRemoteData', recordData),
|
|
], {trace:false});
|
|
|
|
},
|
|
|
|
//=========================================================================
|
|
|
|
'getRecords': function () {
|
|
return Clipperz.Async.callbacks("User.getRecords", [
|
|
MochiKit.Base.method(this, 'getHeaderIndex', 'recordsIndex'),
|
|
MochiKit.Base.methodcaller('records'),
|
|
MochiKit.Base.values
|
|
], {trace:false});
|
|
},
|
|
|
|
'getRecordsLoadingAllData': function () {
|
|
return Clipperz.Async.callbacks("User.getRecordsLoadingAllData", [
|
|
MochiKit.Base.method(this.connection(), 'message', 'getAllRecordDetails'),
|
|
MochiKit.Base.values,
|
|
MochiKit.Base.partial(MochiKit.Base.map, MochiKit.Base.method(this, 'recordWithData')),
|
|
MochiKit.Base.method(this, 'getRecords'),
|
|
], {trace:false});
|
|
},
|
|
|
|
'getRecordsInfo': function (someInfo) {
|
|
return Clipperz.Async.callbacks("User.getRecordsInfo", [
|
|
MochiKit.Base.method(this, 'getRecords'),
|
|
MochiKit.Base.partial(MochiKit.Base.map, Clipperz.Async.collectResults("collectResults", someInfo, {trace:false})),
|
|
Clipperz.Async.collectAll,
|
|
], {trace:false});
|
|
},
|
|
|
|
'recordWithLabel': function (aLabel) {
|
|
return Clipperz.Async.callbacks("User.recordWithLabel", [
|
|
MochiKit.Base.method(this, 'getRecords'),
|
|
MochiKit.Base.partial(Clipperz.Async.deferredFilter, function (aRecord) {
|
|
return Clipperz.Async.callbacks("User.recordWithLabel - check record label", [
|
|
MochiKit.Base.methodcaller('label'),
|
|
MochiKit.Base.partial(MochiKit.Base.operator.eq, aLabel)
|
|
], {trace:false}, aRecord);
|
|
}),
|
|
function (someFilteredResults) {
|
|
var result;
|
|
|
|
switch (someFilteredResults.length) {
|
|
case 0:
|
|
result = null;
|
|
break;
|
|
case 1:
|
|
result = someFilteredResults[0];
|
|
break;
|
|
default:
|
|
//console.log("Warning: User.recordWithLabel('" + aLabel + "') is returning more than one result: " + someFilteredResults.length);
|
|
result = someFilteredResults[0];
|
|
break;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
], {trace:false});
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'getRecord': function (aRecordReference) {
|
|
return Clipperz.Async.callbacks("User.getRecord", [
|
|
MochiKit.Base.method(this, 'getHeaderIndex', 'recordsIndex'),
|
|
MochiKit.Base.methodcaller('records'),
|
|
MochiKit.Base.itemgetter(aRecordReference),
|
|
|
|
Clipperz.Async.deferredIf("record != null", [
|
|
MochiKit.Base.operator.identity
|
|
], [
|
|
function () { throw "Record does not exists"}
|
|
])
|
|
], {trace:false});
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'getRecordDetail': function (aRecord) {
|
|
// return this.connection().message('getRecordDetail', {reference: aRecordReference});
|
|
|
|
return Clipperz.Async.callbacks("User.getRecordDetail", [
|
|
MochiKit.Base.method(this.connection(), 'message', 'getRecordDetail', {reference: aRecord.reference()}),
|
|
function(someInfo) { aRecord.setAttachmentServerStatus(someInfo['attachmentStatus']); return someInfo;}, // Couldn't find a better way...
|
|
], {trace:false});
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'deleteRecord': function (aRecord) {
|
|
return Clipperz.Async.callbacks("User.deleteRecord", [
|
|
MochiKit.Base.method(this, 'getHeaderIndex', 'recordsIndex'),
|
|
MochiKit.Base.methodcaller('deleteRecord', aRecord)
|
|
], {trace:false});
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'createNewRecord': function () {
|
|
return Clipperz.Async.callbacks("User.createNewRecord", [
|
|
MochiKit.Base.method(this, 'getHeaderIndex', 'recordsIndex'),
|
|
MochiKit.Base.methodcaller('createNewRecord')
|
|
], {trace:false});
|
|
},
|
|
|
|
//.........................................................................
|
|
|
|
'createNewRecordFromJSON': function(someJSON) {
|
|
var deferredResult;
|
|
|
|
// TODO: how do we handle attachments?
|
|
deferredResult = new Clipperz.Async.Deferred("User.createNewRecordFromJSON", {trace:false});
|
|
deferredResult.collectResults({
|
|
'recordIndex': MochiKit.Base.method(this, 'getHeaderIndex', 'recordsIndex'),
|
|
'newRecord': [
|
|
MochiKit.Base.method(this, 'createNewRecord'),
|
|
MochiKit.Base.methodcaller('setUpWithJSON', someJSON),
|
|
]
|
|
});
|
|
deferredResult.addCallback(function (someInfo) {
|
|
var record = someInfo['newRecord'];
|
|
var recordIndex = someInfo['recordIndex'];
|
|
|
|
return MochiKit.Base.map(function (aDirectLogin) {
|
|
var configuration = JSON.stringify({
|
|
'page': {'title': aDirectLogin['label']},
|
|
'form': aDirectLogin['formData'],
|
|
'version': '0.2' // correct?
|
|
});
|
|
|
|
return Clipperz.Async.callbacks("User.createNewRecordFromJSON__inner", [
|
|
// TODO: check if we should invoke the create new direct login methon on Record instead of calling it directly on the index
|
|
MochiKit.Base.method(recordIndex, 'createNewDirectLogin', record),
|
|
MochiKit.Base.methodcaller('setLabel', aDirectLogin['label']),
|
|
MochiKit.Base.methodcaller('setBookmarkletConfiguration', configuration),
|
|
MochiKit.Base.methodcaller('setBindings', aDirectLogin['bindingData'], someJSON['currentVersion']['fields']),
|
|
], {'trace': false});
|
|
}, MochiKit.Base.values(someJSON.data.directLogins));
|
|
});
|
|
deferredResult.addCallback(Clipperz.Async.collectAll);
|
|
deferredResult.callback();
|
|
|
|
return deferredResult;
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'cloneRecord': function (aRecord) {
|
|
var result;
|
|
var user = this;
|
|
|
|
return Clipperz.Async.callbacks("User.cloneRecord", [
|
|
MochiKit.Base.method(this, 'hasPendingChanges'),
|
|
Clipperz.Async.deferredIf("User has pending changes", [
|
|
MochiKit.Async.fail
|
|
], [
|
|
MochiKit.Base.method(user, 'createNewRecord'),
|
|
MochiKit.Base.methodcaller('setUpWithRecord', aRecord),
|
|
])
|
|
], {trace:false});
|
|
},
|
|
|
|
//=========================================================================
|
|
|
|
'getDirectLogins': function() {
|
|
var deferredResult;
|
|
|
|
deferredResult = new Clipperz.Async.Deferred("User.getDirectLogins", {trace:false});
|
|
deferredResult.addMethod(this, 'getRecords');
|
|
deferredResult.addCallback(MochiKit.Base.map, MochiKit.Base.compose(MochiKit.Base.values, MochiKit.Base.methodcaller('directLogins')));
|
|
deferredResult.addCallback(MochiKit.Base.flattenArray);
|
|
deferredResult.callback();
|
|
|
|
return deferredResult;
|
|
},
|
|
|
|
//=========================================================================
|
|
|
|
'getOneTimePasswords': function () {
|
|
return Clipperz.Async.callbacks("User.getOneTimePasswords", [
|
|
MochiKit.Base.method(this, 'getHeaderIndex', 'oneTimePasswords'),
|
|
MochiKit.Base.methodcaller('oneTimePasswords'),
|
|
MochiKit.Base.values
|
|
], {trace:false});
|
|
},
|
|
|
|
'getOneTimePasswordsDetails': function() {
|
|
return Clipperz.Async.callbacks("User.getOneTimePasswords", [
|
|
MochiKit.Base.method(this, 'getHeaderIndex', 'oneTimePasswords'),
|
|
MochiKit.Base.methodcaller('oneTimePasswordsDetails', this.connection()),
|
|
], {trace:false});
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'createNewOTP': function () {
|
|
var messageParameters;
|
|
|
|
messageParameters = {};
|
|
return Clipperz.Async.callbacks("User.createNewOTP", [
|
|
MochiKit.Base.method(this, 'getHeaderIndex', 'oneTimePasswords'),
|
|
MochiKit.Base.methodcaller('createNewOTP', this.username(), MochiKit.Base.method(this, 'getPassphrase')),
|
|
MochiKit.Base.methodcaller('encryptedData'),
|
|
MochiKit.Base.partial(function(someParameters, someOTPEncryptedData) {
|
|
someParameters['oneTimePassword'] = someOTPEncryptedData;
|
|
}, messageParameters),
|
|
MochiKit.Base.method(this, 'getPassphrase'),
|
|
MochiKit.Base.method(this, 'prepareRemoteDataWithKey'),
|
|
MochiKit.Base.partial(function(someParameters, someEncryptedRemoteData) {
|
|
someParameters['user'] = someEncryptedRemoteData;
|
|
}, messageParameters),
|
|
MochiKit.Base.method(this.connection(), 'message', 'addNewOneTimePassword', messageParameters)
|
|
], {trace:false});
|
|
},
|
|
|
|
'deleteOTPs': function (aList) {
|
|
var messageParameters;
|
|
|
|
messageParameters = {};
|
|
return Clipperz.Async.callbacks("User.deleteOTPs", [
|
|
MochiKit.Base.method(this, 'getHeaderIndex', 'oneTimePasswords'),
|
|
MochiKit.Base.methodcaller('deleteOTPs', aList),
|
|
MochiKit.Base.partial(function(someParameters, aList) {
|
|
someParameters['oneTimePasswords'] = aList
|
|
}, messageParameters),
|
|
MochiKit.Base.method(this, 'getPassphrase'),
|
|
MochiKit.Base.method(this, 'prepareRemoteDataWithKey'),
|
|
MochiKit.Base.partial(function(someParameters, someEncryptedRemoteData) {
|
|
someParameters['user'] = someEncryptedRemoteData;
|
|
}, messageParameters),
|
|
MochiKit.Base.method(this.connection(), 'message', 'updateOneTimePasswords', messageParameters)
|
|
], {trace:false});
|
|
},
|
|
|
|
'changeOTPLabel': function (aReference, aLabel) {
|
|
return Clipperz.Async.callbacks("User.changeOTPLabel", [
|
|
MochiKit.Base.method(this, 'getHeaderIndex', 'oneTimePasswords'),
|
|
MochiKit.Base.methodcaller('changeOTPLabel', aReference, aLabel),
|
|
MochiKit.Base.method(this,'saveChanges') // Too 'heavy'? Should be moved to MainController to prevent glitch in the UI?
|
|
], {trace:false});
|
|
},
|
|
|
|
//=========================================================================
|
|
|
|
'invokeMethodNamedOnHeader': function (aMethodName, aValue) {
|
|
return Clipperz.Async.collectResults("User.invokeMethodNamedOnHeader [" + aMethodName + "]", {
|
|
'recordIndex': [
|
|
MochiKit.Base.method(this, 'getHeaderIndex', 'recordsIndex'),
|
|
MochiKit.Base.methodcaller(aMethodName, aValue)
|
|
],
|
|
'preferences': [
|
|
MochiKit.Base.method(this, 'getHeaderIndex', 'preferences'),
|
|
MochiKit.Base.methodcaller(aMethodName, aValue)
|
|
],
|
|
'oneTimePasswords': [
|
|
MochiKit.Base.method(this, 'getHeaderIndex', 'oneTimePasswords'),
|
|
MochiKit.Base.methodcaller(aMethodName, aValue)
|
|
]//,
|
|
// 'statistics': [
|
|
// MochiKit.Base.method(this, 'getStatistics'),
|
|
// MochiKit.Base.methodcaller(aMethodName, aValue)
|
|
// ]
|
|
}, {trace:false})();
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'invokeMethodNamedOnRecords': function (aMethodName, aValue) {
|
|
return Clipperz.Async.callbacks("User.invokeMethodNamedOnRecords[" + aMethodName + "]", [
|
|
MochiKit.Base.method(this, 'getRecords'),
|
|
MochiKit.Base.partial(MochiKit.Base.map, MochiKit.Base.methodcaller(aMethodName, aValue)),
|
|
Clipperz.Async.collectAll
|
|
], {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});
|
|
},
|
|
|
|
setPreferences: function(anObject) {
|
|
return Clipperz.Async.callbacks("User.setPreferences", [
|
|
MochiKit.Base.method(this, 'getHeaderIndex', 'preferences'),
|
|
MochiKit.Base.methodcaller('setPreferences', anObject),
|
|
MochiKit.Base.method(this, 'saveChanges')
|
|
], {trace:false});
|
|
},
|
|
|
|
|
|
//-------------------------------------------------------------------------
|
|
/*
|
|
'addNewAttachment': function(anAttachment) {
|
|
console.log("Adding attachment", anAttachment);
|
|
},
|
|
*/
|
|
'uploadAttachment': function(aRecordReference, anAttachmentReference, someData) {
|
|
// TODO: pass a callback to handle onProgress events (and modify Connection accordingly)
|
|
this.connection().message('uploadAttachment', {
|
|
'recordReference': aRecordReference,
|
|
'attachmentReference': anAttachmentReference,
|
|
'data': someData
|
|
});
|
|
},
|
|
|
|
//=========================================================================
|
|
|
|
'hasPendingChanges': function () {
|
|
var deferredResult;
|
|
|
|
deferredResult = new Clipperz.Async.Deferred("User.hasPendingChanges", {trace:false});
|
|
deferredResult.collectResults({
|
|
'header': [
|
|
MochiKit.Base.method(this, 'invokeMethodNamedOnHeader', 'hasPendingChanges'),
|
|
MochiKit.Base.values
|
|
],
|
|
'records': MochiKit.Base.method(this, 'invokeMethodNamedOnRecords', 'hasPendingChanges')
|
|
});
|
|
deferredResult.addCallback(Clipperz.Async.or);
|
|
deferredResult.callback();
|
|
// recordsIndex = legacyHeader;
|
|
// preferences = legacyHeader;
|
|
// oneTimePasswords = legacyHeader;
|
|
|
|
return deferredResult;
|
|
},
|
|
|
|
//=========================================================================
|
|
|
|
'commitTransientState': function () {
|
|
return Clipperz.Async.callbacks("User.commitTransientState", [
|
|
MochiKit.Base.method(this, 'invokeMethodNamedOnHeader', 'commitTransientState'),
|
|
MochiKit.Base.method(this, 'invokeMethodNamedOnRecords', 'commitTransientState'),
|
|
|
|
MochiKit.Base.method(this, 'transientState'),
|
|
// MochiKit.Base.itemgetter('lock'),
|
|
// MochiKit.Base.method(this, 'setServerLockValue'),
|
|
MochiKit.Base.method(this, 'resetTransientState', true)
|
|
], {trace:false});
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'revertChanges': function () {
|
|
return Clipperz.Async.callbacks("User.revertChanges", [
|
|
MochiKit.Base.method(this, 'invokeMethodNamedOnHeader', 'revertChanges'),
|
|
MochiKit.Base.method(this, 'invokeMethodNamedOnRecords', 'revertChanges'),
|
|
MochiKit.Base.method(this, 'resetTransientState', false),
|
|
], {trace:false});
|
|
},
|
|
|
|
//=========================================================================
|
|
|
|
'deleteAllCleanTextData': function () {
|
|
return Clipperz.Async.callbacks("User.deleteAllCleanTextData", [
|
|
MochiKit.Base.method(this, 'invokeMethodNamedOnRecords', 'deleteAllCleanTextData'),
|
|
MochiKit.Base.method(this, 'invokeMethodNamedOnHeader', 'deleteAllCleanTextData'),
|
|
|
|
MochiKit.Base.method(this.data(), 'removeAllData'),
|
|
MochiKit.Base.method(this, 'resetTransientState', false)
|
|
], {trace:false});
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
'hasAnyCleanTextData': function () {
|
|
var deferredResult;
|
|
|
|
deferredResult = new Clipperz.Async.Deferred("User.hasAnyCleanTextData", {trace:false});
|
|
deferredResult.collectResults({
|
|
'header': [
|
|
MochiKit.Base.method(this, 'invokeMethodNamedOnHeader', 'hasAnyCleanTextData'),
|
|
MochiKit.Base.values
|
|
],
|
|
'records': MochiKit.Base.method(this, 'invokeMethodNamedOnRecords', 'hasAnyCleanTextData'),
|
|
'data': MochiKit.Base.bind(function () {
|
|
return MochiKit.Async.succeed(! this.data().isEmpty());
|
|
}, this),
|
|
'transientState': MochiKit.Base.bind(function () {
|
|
return MochiKit.Async.succeed(MochiKit.Base.keys(this.transientState()).length != 0);
|
|
}, this)
|
|
});
|
|
deferredResult.addCallback(Clipperz.Async.or);
|
|
deferredResult.callback();
|
|
|
|
return deferredResult;
|
|
},
|
|
|
|
//=========================================================================
|
|
|
|
'prepareRemoteDataWithKey': function (aKey /*, aCurrentKey*/) {
|
|
var deferredResult;
|
|
var result;
|
|
|
|
result = {};
|
|
deferredResult = new Clipperz.Async.Deferred("User.prepareRemoteDataWithKey", {trace:false});
|
|
deferredResult.addMethod(this, 'invokeMethodNamedOnHeader', 'prepareRemoteDataWithKey', aKey /*, aCurrentKey*/);
|
|
deferredResult.addCallback(MochiKit.Base.bind(function (aResult, someHeaderPackedData) {
|
|
var header;
|
|
|
|
header = {};
|
|
header['records'] = someHeaderPackedData['recordIndex']['records'];
|
|
header['directLogins'] = someHeaderPackedData['recordIndex']['directLogins'];
|
|
header['attachments'] = someHeaderPackedData['recordIndex']['attachments'];
|
|
header['preferences'] = {'data': someHeaderPackedData['preferences']['data']};
|
|
header['oneTimePasswords'] = {'data': someHeaderPackedData['oneTimePasswords']['data']};
|
|
header['version'] = '0.1';
|
|
|
|
aResult['header'] = Clipperz.Base.serializeJSON(header);
|
|
aResult['statistics'] = this._serverData['statistics']; // "someHeaderPackedData['statistics']['data']";
|
|
|
|
return aResult;
|
|
}, this), result);
|
|
deferredResult.addCallback(Clipperz.Async.setItem, result, 'version', Clipperz.PM.Crypto.encryptingFunctions.currentVersion);
|
|
// deferredResult.addCallback(Clipperz.Async.setItem, result, 'lock', this.serverLockValue());
|
|
deferredResult.callback();
|
|
|
|
return deferredResult;
|
|
},
|
|
|
|
'prepareRemoteDataWithKeyFunction': function(aKeyFunction) {
|
|
return new Clipperz.Async.callbacks("User.prepareRemoteDataWithKeyFunction", [
|
|
aKeyFunction,
|
|
MochiKit.Base.method(this, 'prepareRemoteDataWithKey')
|
|
], {'trace': false})
|
|
},
|
|
|
|
//=========================================================================
|
|
|
|
'saveChanges': function () {
|
|
var deferredResult;
|
|
var messageParameters;
|
|
|
|
messageParameters = {};
|
|
|
|
deferredResult = new Clipperz.Async.Deferred("User.saveChangaes", {trace:false});
|
|
|
|
deferredResult.addMethod(this, 'getHeaderIndex', 'recordsIndex');
|
|
deferredResult.addCallback(MochiKit.Base.methodcaller('prepareRemoteDataForChangedRecords'));
|
|
deferredResult.addCallback(Clipperz.Async.setItem, messageParameters, 'records');
|
|
|
|
deferredResult.addMethod(this, 'getPassphrase');
|
|
deferredResult.addMethod(this, 'prepareRemoteDataWithKey');
|
|
deferredResult.addCallback(Clipperz.Async.setItem, messageParameters, 'user');
|
|
|
|
deferredResult.addCallback(MochiKit.Async.succeed, messageParameters);
|
|
deferredResult.addMethod(this.connection(), 'message', 'saveChanges');
|
|
deferredResult.addCallback(MochiKit.Base.update, this.transientState())
|
|
|
|
deferredResult.addMethod(this, 'commitTransientState');
|
|
deferredResult.addErrbackPass(MochiKit.Base.method(this, 'revertChanges'));
|
|
|
|
deferredResult.callback();
|
|
|
|
return deferredResult;
|
|
},
|
|
|
|
//=========================================================================
|
|
__syntaxFix__: "syntax fix"
|
|
});
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
Clipperz.PM.DataModel.User.registerNewAccount = function (anUsername, aPassphraseFunction) {
|
|
var deferredResult;
|
|
var user;
|
|
|
|
user = new Clipperz.PM.DataModel.User({'username':anUsername, 'getPassphraseFunction':aPassphraseFunction});
|
|
|
|
deferredResult = new Clipperz.Async.Deferred("Clipperz.PM.DataModel.User.registerNewAccount", {trace:false});
|
|
deferredResult.addMethod(user, 'registerAsNewAccount');
|
|
deferredResult.addMethod(user, 'login');
|
|
deferredResult.addCallback(MochiKit.Async.succeed, user);
|
|
deferredResult.callback();
|
|
|
|
return deferredResult;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
Clipperz.PM.DataModel.User.exception = {
|
|
LoginFailed: new MochiKit.Base.NamedError("Clipperz.PM.DataModel.User.exception.LoginFailed"),
|
|
CredentialUpgradeFailed: new MochiKit.Base.NamedError("Clipperz.PM.DataModel.User.exception.CredentialUpgradeFailed")
|
|
};
|
|
|
|
//-----------------------------------------------------------------------------
|