/*
@author pavel.mash
*/
var Q = require('./libs/q');
var _ = require('./libs/lodash/lodash');
var EventEmitter = require('./events');
var UB = require('./UB');
var UBSession = require('./UBSession');
var LocalDataStore = require('./LocalDataStore');
var UBDomain = require('./UBDomain');
var UBCache = require('./UBCache');
var CryptoJS = require('./libs/CryptoJS/core');
var UBNativeDSTUCrypto = require('./UBNativeDSTUCrypto');
require('./libs/CryptoJS/md5');
require('./libs/CryptoJS/sha256');
require('./libs/CryptoJS/enc-base64');
require('./libs/CryptoJS/lib-typedarrays');
//regular expression for URLs server not require authorization.
var NON_AUTH_URLS_RE = /(\/|^)(models|auth|getAppInfo|downloads)(\/|\?|$)/;
//regular expression for URLs server not require encryption. Note - all non-auth method not require encryption also
var NON_ENCRYPTED_URLS_RE = /(\/|^)(initEncryption)(\/|\?|$)/;
// all request passed in this timeout to run will be send into one runList server method execution
var BUFFERED_DELAY = 20;
var AUTH_METHOD_URL = 'auth';
/**
* @classdesc
*
* Connection to then UnityBase server.
*
* In case host set to value other then `location.host` server must be configured to accept
* <a href="https://developer.mozilla.org/en-US/docs/HTTP/Access_control_CORS">CORS</a> requests.
* Usually it done via "HTTPAllowOrigin" server configuration option.
*
* Usage sample:
*
var conn = new UBConnection({
host: 'http://127.0.0.1:888',
requestAuthParams: function(conn, isRepeat){
if (isRepeat){
throw new UB.UBAbortError('invalid')
} else {
return Q.resolve({authSchema: 'UBIP', login: 'admin'})
}
}
});
conn.query({entity: 'uba_user', method: 'select', fieldList: ['ID', 'name']}).done(UB.logDebug);
* This class mixes an EventEmitter, so you can subscribe for `authorized` and `authorizationFail` events:
*
conn.on('authorizationFail', function(reason){
// indicate user credential is wrong
});
conn.on('authorized', function(ubConnection, session, authParams){console.debug(arguments)} );
*
* @class
* @mixes EventEmitter
* @param {Object} connectionParams connection parameters
* @param {String} connectionParams.host UnityBase server host
* @param {String} [connectionParams.appName='/'] UnityBase application to connect to
* @param {Function} connectionParams.requestAuthParams Handler to log in.
* Must return promise & fulfill it by authorization parameters: {authSchema: authType, login: login, password: password }
* for openIDConnect must be fulfilled with {data: uData, secretWord: ???}
* Called with arguments: {UBConnection} conn, {Boolean} isRepeat;
* isRepeat === true in case first auth request is invalid
* @param {String} [connectionParams.protocol] either 'https' or 'http' (default)
*/
function UBConnection(connectionParams){
var
baseURL,
host = connectionParams.host || 'http://localhost:888',
appName = connectionParams.appName || '/',
serverURL,
requestAuthParams = connectionParams.requestAuthParams,
/*
* Current session (Promise). Result of {@link UBConnection#auth auth} method
* {@link UBConnection#xhr} use this promise as a first `then` in call chain. In case of 401 response
* authPromise recreated.
*/
authPromise;
EventEmitter.call(this);
_.assign(this, EventEmitter.prototype);
if (appName.charAt(0) !== '/') {
appName = '/' + appName;
}
if (!requestAuthParams) {
throw new Error('connectionParams.requestAuthParams is required');
}
serverURL = host + appName;
/** UB Server URL with protocol and host.
* @type {string}
* @readonly
*/
this.serverUrl = serverURL;
baseURL = (window.location.origin === host) ? appName : serverURL;
if (baseURL.charAt(baseURL.length-1) !== '/') baseURL = baseURL + '/';
/**
* The base of all urls of your requests. Will be prepend to all urls while call UB.xhr
* @type {String}
* @readonly
*/
this.baseURL = baseURL;
/** UB application name
* @type {String}
* @readonly
*/
this.appName = appName;
/** Result of last key agreement. Resolved to object,
* contain time when it was done inside
* - **doneTime** property
* Usually this is result of UBConnection.doKeyExchange method call
* @private
* @type {Promise}
*/
this.exchangeKeysPromise = null;
/** UBNativeDSTUCrypto instance used for encryption
* @type {UBNativeDSTUCrypto}
*/
this.channelEncryptor = null;
/** UBNativeIITCrypto instance used for PKI relative operations (read keys / signatures)
* @private
* @readonly
* @type {UBNativeIITCrypto}
*/
this._pki = null;
/**
* Check current connection use PKI
* @protected
* @returns {boolean}
*/
this.isPKIReady = function(){
return this._pki !== null;
};
this.cache = null;
/**
* Last successful login name. Filled AFTER first 401 response
* @type {String}
*/
this.lastLoginName = '';
/**
* Cache flag to enable a hack appending the current timestamp
* to your requests to prevent IE from caching them and always returning the same result.
* If "true", will set the param with the name "_"
* If a string, will use it as the param name
* @type {Boolean|String}
*/
this.useCacheForXHR = window && (window.ActiveXObject || 'ActiveXObject' in window);
this._bufferedRequests = [];
this._bufferTimeoutPromise = null;
/**
* Is user currently logged in. There is no guaranty what session actually exist in server.
* @returns {boolean}
*/
this.isAuthorized = function(){
return (authPromise !== null) && authPromise.isFulfilled();
};
/**
* Return current user logon name or '' in case not logged in
* @returns {String}
*/
this.userLogin = function(){
return this.isAuthorized() ? authPromise.valueOf().logonname : '';
};
/**
* Return current user language or 'en' in case not logged in
* @returns {String}
*/
this.userLang = function(){
return this.userData('lang');
};
/**
* Return custom data for logged in user, or {lang: 'en'} in case not logged in
*
* If key is provided - return only key part of user data:
*
* $App.connection.userData('lang');
* // or the same but dedicated alias
* $App.connection.userLang()
*
* @param {String} [key] Optional key
* @returns {*}
*/
this.userData = function(key){
var uData;
if (this.isAuthorized()){
uData = authPromise.valueOf().userData;
} else {
console.warn('Connection is not authorized. A fake userData will be returned');
uData = {lang: 'en'};
}
return key ? uData[key] : uData;
};
var authDefer;
/**
* The starter method for all authorized requests to UB server. Return authorization promise resolved to {@link UBSession}.
* In case unauthorized:
*
* - call requestAuthParams method passed to UBConnection constructor to retrieve user credentials
* - call {@link UBConnection#doAuth} method
*
* Used inside {@link UBConnection#xhr}, therefore developer rarely call it directly.
* @method
* @param {boolean} [isRepeat] in case user provide wrong credential - we must show logon window
* @returns {Promise} Resolved to {@link UBSession} is auth success or rejected to `{errMsg: string, errCode: number, errDetails: string}` if fail
*/
this.authorize = function(isRepeat){
var me = this;
if (!authPromise || isRepeat){
if (!isRepeat) {
authDefer = Q.defer();
authPromise = authDefer.promise;
this.exchangeKeysPromise = null;
}
requestAuthParams(this, isRepeat).then(function(authParams){
return me.doAuth(authParams).then(function(session){
/**
* Fired for {@link UBConnection} instance after success authorization. Accept 3 args (conn: UBConnection, session: UBSession, authParams)
* @event authorized
*/
me.emit('authorized', me, session, authParams);
return session;
});
}).then(function(session){
authDefer.resolve(session);
}).fail(function(reason){
if (!reason || !(reason instanceof UB.UBAbortError)){
/**
* Fired for {@link UBConnection} instance in case of bad authorization Accept 1 args (reason)
* @event authorizationFail
*/
me.emit('authorizationFail', reason);
}
me.authorize(true);
}).done();
}
return authPromise;
};
/**
* Clear current user authorization promise. Next request repeat authorization
* @private
*/
this.authorizationClear = function(){
this.lastLoginName = this.userLogin();
authPromise = null;
};
/**
* UBIP Auth schema implementation
* @param authParams
* @returns {Promise}
*/
this.authHandshakeUBIP = function(authParams) {
if (!authParams.login) {
return Q.reject({errMsg: 'invalid user name'});
}
return this.post(AUTH_METHOD_URL, '', {headers: {Authorization: authParams.authSchema + ' ' + authParams.login}});
};
/**
* openID Connect auth schema.
* This function act as a proxy but change authSchema back to 'UB' for authorization token generation
* @param authParams
* @return {*}
*/
this.authHandshakeOpenIDConnect = function(authParams) {
return Q(authParams).then(function(authParams){
authParams.authSchema = 'UB';
return authParams;
});
};
/**
* UB Auth schema implementation
* @param authParams
* @returns {Promise}
*/
this.authHandshakeUB = function(authParams){
var me = this,
secretWord;
if (!authParams.login || !authParams.password){
return Q.reject({errMsg: 'invalid user name or password'});
}
return me.post(AUTH_METHOD_URL, '', {
params: {
AUTHTYPE: authParams.authSchema,
userName: authParams.login
}
}).then(function(resp){
var
serverNonce, clientNonce, pwdHash, pwdForAuth, realm,
SHA256 = CryptoJS.SHA256,
request = {
params: {
AUTHTYPE: authParams.authSchema,
userName: authParams.login,
password: ''
}
};
clientNonce = SHA256(new Date().toISOString().substr(0, 16)).toString();
request.params.clientNonce = clientNonce;
if (resp.data.connectionID){
request.params.connectionID = resp.data.connectionID;
}
// LDAP AUTH?
realm = resp.data.realm;
if (realm){
serverNonce = resp.data.nonce;
if (!serverNonce) {
throw new Error('invalid LDAP auth response');
}
if (resp.data.useSasl) {
pwdHash = CryptoJS.MD5(authParams.login.split('\\')[1].toUpperCase() + ':' + realm + ':' + authParams.password);
// we must calculate md5(login + ':' + realm + ':' + password) in binary format
pwdHash.concat(CryptoJS.enc.Utf8.parse(':' + serverNonce + ':' + clientNonce));
pwdForAuth = CryptoJS.MD5(pwdHash).toString();
secretWord = pwdForAuth; //:( medium unsecured
} else {
pwdForAuth = btoa(authParams.password);
secretWord = pwdForAuth; //todo - very unsecured!!
}
} else {
serverNonce = resp.data.result;
if (!serverNonce) {
throw new Error('invalid auth response');
}
pwdHash = SHA256('salt' + authParams.password).toString();
var appForAuth = appName === '/' ? '/' : appName.replace(/\//g, '');
pwdForAuth = SHA256(appForAuth.toLowerCase() + serverNonce + clientNonce + authParams.login + pwdHash).toString();
secretWord = pwdHash;
}
request.params.password = pwdForAuth;
return me.post(AUTH_METHOD_URL, '', request).then(function(response){
response.secretWord = secretWord;
return response;
});
});
};
/**
* CERT auth schema implementation
* @param authParams
* @returns {Promise}
*/
this.authHandshakeCERT = function(authParams) {
var
me = this, pki, secretWord,
urlParams={
AUTHTYPE: authParams.authSchema
};
if(authParams.login) {
urlParams.userName = authParams.login;
}
if (authParams.password){
urlParams.password = authParams.password;
}
if (authParams.registration){
urlParams.registration = authParams.registration;
}
return me.pki().then(function(pkiInit){
pki = pkiInit;
//noinspection JSCheckFunctionSignatures
return pki.readPK(me);
}).then(function(certInfo){
var reqData = certInfo.ownIITCert;
// in case we have different certificates for signing and encryption - pass them all
if (certInfo.ownIITEncryptCert && certInfo.ownIITEncryptCert !== '') {
reqData = [certInfo.ownIITCert, certInfo.ownIITEncryptCert, certInfo.ownIITEncryptSignature].join(' ');
}
return me.xhr({
url: AUTH_METHOD_URL,
method: 'POST',
headers: {'Content-Type': 'application/octet-stream'},
params: urlParams,
responseType: 'arraybuffer',
data: reqData
});
}).then(function(){
throw new UB.UBError('msgInvalidCertAuth');
}, function(resp){
var aValues, authData, serverMessage;
if((resp instanceof Error) || (resp instanceof UB.UBError)){
throw resp;
}
if (resp.status !== 401){
if (resp.data && resp.data instanceof ArrayBuffer && resp.data.byteLength > 1 ){
try {
//noinspection JSCheckFunctionSignatures
var respObj = JSON.parse(String.fromCharCode.apply(null, new Uint8Array( resp.data)));
if ((respObj.errCode === 65 || respObj.errCode === 0) && respObj.errMsg && /<<<.*>>>/.test(respObj.errMsg)){
serverMessage = respObj.errMsg.match(/<<<(.*)>>>/)[1];
}
} catch(err) {}
if (serverMessage){
throw new UB.UBError(serverMessage);
}
}
throw new UB.UBError('msgInvalidCertAuth');
}
// begin phrase 2 of auth.
// wait for response WWW-Authenticate: CERT connectionId server_certificate
// and encrypted by client certificate secret word in body
authData = resp.headers('WWW-Authenticate');
if (!authData && (authData.substr(0,5) !== 'CERT ')) {
throw new Error('invalidCertAuthRespAuthType');
}
aValues = authData.split(' ');
if (aValues.length !== 3){ throw new Error('invalidCertAuthResponse'); }
urlParams.CONNECTIONID = aValues[1];
return pki.setServerCertificate(aValues[2]).then(function() {
return UB.base64FromAny(resp.data);
}).then(function(envelop){
if (!envelop || (envelop === '')){ throw new Error('invalidCertAuthEnvelop'); }
// decrypt secret word using client private key
return pki.decryptEnvelope( envelop, true);
}).then(function(secretWordB64){
// memorize it
secretWord = atob(secretWordB64);
// encrypt secret word with server public key
return pki.encryptEnvelope( secretWordB64, true);
}).then(function(encryptEnvelopeRes){
var envelop = UB.base64toArrayBuffer(encryptEnvelopeRes);
if (envelop.byteLength === 0) { throw new Error('invalidCertAuthEnvelopOut'); }
// repeat request with encrypted secret word
return me.xhr({
url: AUTH_METHOD_URL,
method: 'POST',
headers: {'Content-Type': 'application/octet-stream'},
params: urlParams,
data: envelop
});
}).then(function(response){
response.secretWord = secretWord;
return response;
});
})
.fin(function(){
if(pki) {
pki.closePK().done();
}
});
};
/*jslint bitwise: false */
/**
* Do authentication in UnityBase server. Usually called from UBConnection #authorize method in case authorization expire or user not authorized.
* Resolve to {@link UBSession} session object.
*
* @private
* @param {Object} authParams
* @param {String} [authParams.authSchema] Either 'UB' (default) or 'CERT'. On case of CERT UBDesktop service NPI extension must be installed in browser
* @param {String} [authParams.login] Optional login
* @param {String} [authParams.password] Optional password
* @returns {Promise} Authentication promise. Resolved to {@link UBSession} is auth success or rejected to {errMsg: string, errCode: number, errDetails: string} if fail
*/
this.doAuth = function(authParams){
var promise,
me = this;
authParams.authSchema = authParams.authSchema || 'UB';
if (me.isAuthorized()){
return Q.reject({errMsg: 'invalid auth call', errDetails: 'contact developers'});
}
switch(authParams.authSchema){
case 'UB':
promise = me.authHandshakeUB(authParams);
break;
case 'CERT':
promise = me.authHandshakeCERT(authParams);
break;
case 'UBIP':
promise = me.authHandshakeUBIP(authParams);
break;
case 'OpenIDConnect':
promise = me.authHandshakeOpenIDConnect(authParams);
break;
case 'Negotiate':
promise = me.post(AUTH_METHOD_URL, '', {
params: {
USERNAME: '',
AUTHTYPE: authParams.authSchema
}
}).then(function(resp){
resp.secretWord = resp.data.logonname;
return resp;
});
break;
default:
promise = Q.reject({errMsg: 'invalid authentication schema ' + authParams.authSchema});
break;
}
promise = promise.then(function(authResponse){
var ubSession = new UBSession(authResponse.data, authResponse.secretWord, authParams.authSchema);
var userData = ubSession.userData;
if (!userData.lang || UB.appConfig.supportedLanguages.indexOf(userData.lang) === -1){
userData.lang = UB.appConfig.supportedLanguages[0];
}
return ubSession;
}, function(rejectReason){
var errInfo = {}, codeMsg;
if (!(rejectReason instanceof Error)) {
if (rejectReason.data) {
errInfo = {
errMsg: rejectReason.data.errMsg,
errCode: rejectReason.data.errCode,
errDetails: rejectReason.data.errMsg
};
}
if (rejectReason.status === 403) {
errInfo.errMsg = (authParams.authSchema === 'UB') ? 'msgInvalidUBAuth' : 'msgInvalidCertAuth';
} else {
if (!errInfo.errMsg) { errInfo.errMsg = 'unknownError'; } //internalServerError
}
if (rejectReason.status === 0) {
errInfo.errDetails = 'network error';
}
if ( /<<<.*>>>/.test(errInfo.errMsg)){
errInfo.errMsg = errInfo.errMsg.match(/<<<(.*)>>>/)[1];
}
codeMsg = me.serverErrorByCode(errInfo.errCode);
if (codeMsg){
errInfo.errDetails = codeMsg + ' ' + errInfo.errDetails;
if (UB.i18n(codeMsg) !== codeMsg){
errInfo.errMsg = codeMsg;
}
}
throw new UB.UBError(errInfo.errMsg, errInfo.errDetails, errInfo.errCode );
} else {
throw rejectReason; //rethrow error
}
});
return promise;
};
this.recordedXHRs = [];
/**
* Set this to `true` to memorize all requests to this.recordedXHRs array (for debug only!).
* @type {Boolean}
*/
this.recorderEnabled = false;
}
/**
* Initialize client cache. Called from application after obtain userDbVersion (in case of Ext-based client called from {@link UB.core.UBApp#launch}.
*
* - recreate Indexed Db database if version changed
* - create instance of UBCache (accessible via {@link UBConnection#cache UBConnection.cache} property) and clear UBCache.SESSION store.
*
* @param {Number} userDbVersion Indexed DB database version required for current application
* @returns {Promise}
*/
UBConnection.prototype.initCache = function(userDbVersion){
var me = this;
/**
* @property {UBCache} cache
* @readonly
* @type {UBCache}
*/
me.cache = new UBCache(me.baseURL, userDbVersion);
/**
* List of keys, requested in the current user session.
* Cleared each time login done
* @protected
* @property {Object} cachedSessionEntityRequested
*/
me.cachedSessionEntityRequested = {};
//clear use session store
return me.cache.clear(UBCache.SESSION);
};
/**
* Calculate cache key for request. This key is used to store data inside UBCache
* @param {String} root This is usually entity name
* @param {Array<string>} [attributes] if present - add attributes hash. This is usually array of entity attributes we want to put inside cache
* @returns {String}
*/
UBConnection.prototype.cacheKeyCalculate = function(root, attributes){
var me = this,
keyPart = [me.userLogin().toLowerCase(), me.userLang(), root];
if (Array.isArray(attributes)){
keyPart.push(CryptoJS.MD5(JSON.stringify(attributes)).toString());
}
return keyPart.join('#').replace(/[\\:\.]/g, '#'); // replace all :, \ -> #;
};
/**
* Refresh all cache occurrence for root depending on cacheType:
*
* - if `Session` - clear indexedDB for this root.
* - if `SessionEntity` - remove entry in {@link UBConnection#cachedSessionEntityRequested}
* - else - do nothing
* @param {String} root Root part of cache key. The same as in {@link UBConnection#cacheKeyCalculate}
* @param {String} cacheType One of {@link UBCache#cacheTypes}
* @returns {Promise}
*/
UBConnection.prototype.cacheOccurrenceRefresh = function(root, cacheType){
var me = this,
cacheKey, machKeys, machRe,
promise = Q.resolve(true),
domain, mixin, domainMixin ;
if (cacheType === UBCache.cacheTypes.Session || cacheType === UBCache.cacheTypes.SessionEntity ) {
domain = this.domain.get(root);
if (domain && domain.hasMixin('unity') ){
mixin = domain.mixin('unity');
domainMixin = this.domain.get(mixin.entity);
if (domainMixin && (mixin.entity !== root) && (domainMixin.cacheType !== UBCache.cacheTypes.None)) {
promise = promise.then(function(){
me.cacheOccurrenceRefresh(mixin.entity, domainMixin.cacheType);
});
}
}
cacheKey = me.cacheKeyCalculate(root);
machRe = new RegExp('^' + cacheKey);
machKeys = Object.keys(me.cachedSessionEntityRequested).filter(function (item) {
return machRe.test(item);
});
machKeys.forEach(function (key) {
delete me.cachedSessionEntityRequested[key];
});
if (cacheType === UBCache.cacheTypes.Session) {
promise = promise.then(function(){
me.cache.removeIfMach(machRe, UBCache.SESSION);
});
}
}
return promise;
};
/**
* Remove all cache occurrence for root depending on cacheType:
*
* - clear indexedDB for this root.
* - remove entry in {@link UBConnection#cachedSessionEntityRequested}
* @param {String} root Root part of cache key. The same as in {@link UBConnection#cacheKeyCalculate}
* @param {String} cacheType One of {@link UBCache#cacheTypes}
* @returns {Promise}
*/
UBConnection.prototype.cacheOccurrenceRemove = function(root, cacheType){
var me = this,
cacheKey, machKeys, machRe,
cacheStore;
cacheKey = me.cacheKeyCalculate(root);
machRe = new RegExp('^'+cacheKey);
machKeys = Object.keys(me.cachedSessionEntityRequested).filter(function(item){
return machRe.test(item);
});
machKeys.forEach(function(key){
delete me.cachedSessionEntityRequested[key];
});
cacheStore = (cacheType === UBCache.cacheTypes.Session) ? UBCache.SESSION : UBCache.PERMANENT;
return me.cache.removeIfMach(machRe, cacheStore);
};
/**
* Clear all local cache (indexedDB session & permanent and UBConnection.cachedSessionEntityRequested)
* @returns {Promise}
*/
UBConnection.prototype.cacheClearAll = function(){
var me = this;
Object.keys(me.cachedSessionEntityRequested).forEach(function(item){
delete me.cachedSessionEntityRequested[item];
});
return Q.all([me.cache.clear(UBCache.SESSION), me.cache.clear(UBCache.PERMANENT)]);
};
/**
* Return instance of UBNativeIITCrypto for PKI operation
* @returns {UBNativeIITCrypto}
*/
UBConnection.prototype.pki = function(){
var me = this,
rDefer = Q.defer();
if (!me._pki) {
me._pki = new UBNativeIITCrypto();
me._pkiInit = me._pki.init();
}
me._pkiInit.then(function(){
rDefer.resolve(me._pki);
},function(reason){
rDefer.reject(reason);
});
return rDefer.promise;
};
/**
* Perform key exchange in case of encrypted communication
* @param session
* @returns {Promise}
*/
UBConnection.prototype.exchangeKeys = function(session){
var doneAt, now, me = this;
if (!me.exchangeKeysPromise){
me.exchangeKeysPromise = this.doKeyExchange(session);
} else {
// check session key is near to expire. do key exchange if yes
if (me.exchangeKeysPromise.isFulfilled() && me.encryptionKeyLifetime > 0 ){
doneAt = me.exchangeKeysPromise.valueOf().doneTime;
now = (new Date()).getTime();
if ((now-doneAt)/1000 > this.encryptionKeyLifetime - 10){
me.exchangeKeysPromise = me.doKeyExchange(session);
}
}
}
return me.exchangeKeysPromise;
};
/**
* Shortcut method to perform authorized `GET` request to application we connected
* @param {string} url Relative or absolute URL specifying the destination of the request
* @param {Object=} [config] Optional configuration object as in {@link UB.xhr}
* @returns {Promise} Future object
*/
UBConnection.prototype.get = function(url, config) {
return this.xhr(_.assign(
{},
config,
{
method: 'GET',
url: url
}
));
};
/**
* Shortcut method to perform authorized `POST` request to application we connected
* @param {string} url Relative or absolute URL specifying the destination of the request
* @param {*} data Request content
* @param {Object=} [config] Optional configuration object as in {@link UB.xhr}
* @returns {Promise} Future object
*/
UBConnection.prototype.post = function(url, data, config) {
return this.xhr(_.assign(
{}, config,
{
method: 'POST',
url: url,
data: data
}
));
};
/**
* Shortcut method to perform authorized/encrypted request to application we connected.
* Will:
*
* - add Authorization header
* - add {@link UBConnection#baseURL} to config.url
* - call {@link UB.xhr}
* - in case server return 401 clear current authorization and call {@link UBConnection#authorize). Then repeat request
*
* @param {Object} config Request configuration as described in {@link UB.xhr}
* @return {Promise}
*/
UBConnection.prototype.xhr = function(config) {
var me = this,
cfg = _.assign({headers: {}}, config),
url = cfg.url,
promise, isBase64 = false;
if (me.recorderEnabled){
me.recordedXHRs.push(config);
}
//prepend baseURl only if not already prepended
if (url.length < me.baseURL.length || url.substring(0, me.baseURL.length) !== me.baseURL) {
cfg.url = me.baseURL + cfg.url;
}
if (NON_AUTH_URLS_RE.test(url) ){ //request not require authentication - pass is as is
promise = UB.xhr(cfg);
} else {
promise = me.authorize();
if (me.trafficEncryption && !NON_ENCRYPTED_URLS_RE.test(url)){
promise = promise.then(function(session) {
return me.exchangeKeys(session);
}).then(function() {
var dataType;
// must be call to me.exchangeKeys - exchangeKeys use 'this' inside
//return Q.delay(me.exchangeKeys(session), 20).then(function(){
// string and stringified objects is not need to be base64 encoded. Anything else - must
dataType = typeof cfg.data;
if (( dataType === 'string') || ((dataType === 'object') && !(cfg.data instanceof Blob) && !(cfg.data.byteLength))) {
return JSON.stringify(cfg.data);
} else {
isBase64 = true;
return UB.base64FromAny(cfg.data);
}
}).then(function(sendData){
var
compress;
cfg.transformResponse = decryptResponse;
cfg.responseType = 'arraybuffer';
cfg.transformRequest = function (data){return data;}; //NOP
if (Boolean(sendData)) {
compress = (sendData.length > 1000);
return me.channelEncryptor.encryptToArray(sendData, isBase64, compress)
.then(function(encryptToArrayRes){
cfg.headers['Content-Encoding'] = compress ? 'UBEZ' : 'UBE';
cfg.data = new Uint8Array(encryptToArrayRes);
});
} else {
return true;
}
});
}
promise = promise.then(function(){
// we must repeat authorize to obtain new session key ( because key exchange may happens before)
return me.authorize().then(/** @param {UBSession} session*/ function(session) {
cfg.headers.Authorization = session.authHeader();
return UB.xhr(cfg);
});
}).fail(function (reason) { // in case of 401 - do auth and repeat request
var errMsg = '', errDetails, errCode;
if (reason.status === 401) {
UB.logDebug('unauth: %o', reason);
if (me.isAuthorized()) {
me.authorizationClear();
}
//reason.config.url: "/bla-bla/logout"
if (reason.config.url && /\/logout/.test(reason.config.url)){
me.lastLoginName = '';
} else {
UB.xhr.allowRequestReiteration(); // prevent a monkeyRequestsDetected error during relogon [UB-1699]
return me.xhr(config);
}
}
if (reason.data && reason.data.hasOwnProperty('errCode')){ // this is server side error response
errCode = reason.data.errCode;
errDetails = errMsg = reason.data.errMsg;
if (/<<<.*>>>/.test(errMsg)){ // this is custom error
errMsg = UB.i18n(errMsg.match(/<<<(.*)>>>/)[1]); //extract rear message and translate
}
/**
* Fired for {@link UBConnection} instance in case user password is expired. The only valid endpoint after this is `changePassword`
* @event passwordExpired
*/
if ((errCode === 72) && me.emit('passwordExpired')) {
throw new UB.UBAbortError();
}
throw new UB.UBError(errMsg, errDetails, errCode);
} else {
throw reason; //!Important - rethrow the reason is important. Do not create a new Error here
}
});
}
return promise;
function decryptResponse(data, headers) {
var encoding = (headers('content-encoding') || '').toUpperCase(),
compressed = encoding === 'UBEZ',
encrypted = compressed || (encoding === 'UBE'),
promise,
jsonContent = (headers('content-type') || '').indexOf('json') >= 0,
resAsBase64 = (config.responseType === "arraybuffer");
promise = Q.resolve(data);
if (encrypted){
promise = promise.then(function(dataPromise){
return me.channelEncryptor.decryptArr(dataPromise, resAsBase64, compressed);
});
}
return promise.then(function(dataPromise){
if (config.responseType === "arraybuffer"){
dataPromise = UB.base64toArrayBuffer(dataPromise);
}
if (typeof dataPromise === 'string' && jsonContent) {
try {
dataPromise = JSON.parse(dataPromise);
} catch (err){
console.log(dataPromise);
throw err;
}
}
return dataPromise;
});
}
};
/**
* Base64 encoded server certificate
* @property {String} serverCertificate
* @readonly */
/** Lifetime (in second) of session encryption
* @property {Numeric} encryptionKeyLifetime
* @readonly */
/**
* Possible server authentication method
* @property {Array.<string>} authMethods
* @readonly */
/**
* Retrieve application information. Usually this is first method developer must call after create connection
* @method
* @returns {Promise} Promise resolved to result of getAppInfo method
*/
UBConnection.prototype.getAppInfo = function(){
var me = this, appInfo;
return me.get('getAppInfo') //non-auth request
.then(function(resp) {
appInfo = resp.data;
/** Is server require content encryption
* @property {Boolean} trafficEncryption
* The base of all urls of your requests. Will be prepend to all urls.
* @readonly */
Object.defineProperty(me, 'trafficEncryption', {enumerable: true, writable: false, value: appInfo.trafficEncryption || false});
/** The server certificate for cryptographic operations (base46 encoded)
* @property {Boolean} serverCertificate
* @readonly */
Object.defineProperty(me, 'serverCertificate', {enumerable: true, writable: false, value: appInfo.serverCertificate || ''});
Object.defineProperty(me, 'encryptionKeyLifetime', {enumerable: true, writable: false, value: appInfo.encryptionKeyLifetime || 0});
Object.defineProperty(me, 'authMethods', {enumerable: true, writable: false, value: appInfo.authMethods});
/**
* An array of WebSocket protocol names supported by server
* @property {Array<String>} supportedWSProtocols
*/
Object.defineProperty(me, 'supportedWSProtocols', {enumerable: true, writable: false, value: appInfo.supportedWSProtocols || []});
/** UnityBase server version
* @property {String} serverVersion
* @readonly */
Object.defineProperty(me, 'serverVersion', {enumerable: true, writable: false, value: appInfo.serverVersion || ''});
UB.apply(UB.appConfig, appInfo.uiSettings.adminUI);
return appInfo;
});
};
/**
* Retrieve domain information from server. Promise resolve instance of UBDomain.
* @param {Function} [callBack] This parameter will be deleted in next version
* @returns {Promise}
*/
UBConnection.prototype.getDomainInfo = function(callBack) {
var me = this;
return me.get('getDomainInfo').then(function(response) {
var
result = response.data,
domain;
domain = new UBDomain(result);
/**
* Domain information. It will be defined after resolved the Promise returned by function {@link UBConnection#getDomainInfo getDomainInfo} .
* @property {UBDomain} domain
* @type {UBDomain}
*/
me.domain = domain;
if (callBack){
callBack(result, domain);
}
return domain;
});
};
/**
* If connection require encryption then initialize UBConnection.channelEncryptor
* @return {Promise}
*/
UBConnection.prototype.initEncriptionIfNeed = function(){
var me = this;
if (me.trafficEncryption && !me.channelEncryptor) {
if (!me.serverCertificate) {
me.channelEncryptor = null;
return Q.reject(new Error('During call to getAppInfo server not return certificate required for encrypted communication'));
} else {
me.channelEncryptor = new UBNativeDSTUCrypto();
return me.channelEncryptor.init(me.serverCertificate);
}
} else {
return Q.resolve(true);
}
};
/**
* Generate session encryption key and send it to server inside envelope.
* Must be called for encrypted communication AFTER call to
* {@link UBConnection#getAppInfo getAppInfo} and authenticate user
* @returns {Promise}
* @private
*/
UBConnection.prototype.doKeyExchange = function(session){
var me = this, initEncryptionRequest;
if (!me.trafficEncryption){
return Q.resolve({
doneTime: 0,
trafficEncryption: me.trafficEncryption
});
}
if (!me.isAuthorized()) {
return Q.reject({errMsg: 'must be authorized before do key agreement'});
}
//TODO - wait for pending requests before call to getEnvelopeWithKey
//var envelope =
return me.channelEncryptor.getEnvelopeWithKey()
.then(function(envelope){
initEncryptionRequest = {
url: 'initEncryption',
method: 'POST',
data: envelope,
headers: {
'Content-Type': 'application/octet-stream',
'Authorization': 'UB ' + session.signature()
}
};
return me.xhr(initEncryptionRequest);
})
.then(function(){
return Q.fulfill({
doneTime: new Date().getTime(),
trafficEncryption: me.trafficEncryption
});
});
};
/**
* Process buffered requests from this._bufferedRequests
* @private
*/
UBConnection.prototype.processBuffer = function processBuffer(){
var me = this,
bufferCopy = me._bufferedRequests;
// get ready to new buffer queue
me._bufferedRequests = []; me._bufferTimeoutPromise = null;
me.post('ubql', _.pluck(bufferCopy, 'request')).then(
function(responses){
// we expect responses in order we send requests to server
bufferCopy.forEach(function(bufferedRequest, num){
bufferedRequest.deferred.resolve(responses.data[num]);
});
}, function(failReason){
bufferCopy.forEach(function(bufferedRequest){
bufferedRequest.deferred.reject(failReason);
});
}).done();
};
/**
* Promise of running UBQL command(s) (asynchronously).
* The difference from {@link UBConnection.post} is:
*
* - ability to buffer request: can merge several `query` in the 20ms period into one ubql call
*
* For well known UnityBase methods use aliases (addNew, select, insert, update, doDelete)
* @param {Object} ubq Request to execute
* @param {String} ubq.entity Entity to execute the method
* @param {String} ubq.method Method of entity to executed
* @param {Array.<String>} [ubq.fieldList]
* @param {Object} [ubq.whereList]
* @param {Object} [ubq.execParams]
* @param {Number} [ubq.ID]
* @param {Object} [ubq.options]
* @param {String} [ubq.lockType]
* @param {Boolean} [ubq.__skipOptimisticLock] In case this parameter true and in the buffered
* @param {Boolean} [ubq.__nativeDatasetFormat]
* @param {Boolean} [allowBuffer] Allow buffer this request to single runList. False by default
* @method
* @returns {Promise}
*
* Example:
*
* //this two execution is passed to single ubql server execution
* $App.connection.query({entity: 'uba_user', method: 'select', fieldList: ['*']}, true).done(UB.logDebug);
* $App.connection.query({entity: 'ubm_navshortcut', method: 'select', fieldList: ['*']}, true).done(UB.logDebug);
*
* //but this request is passed in separate ubql (because allowBuffer false in first request
* $App.connection.query({entity: 'uba_user', method: 'select', fieldList: ['*']}).done(UB.logDebug);
* $App.connection.query({entity: 'ubm_desktop', method: 'select', fieldList: ['*']}, true).done(UB.logDebug);
*/
UBConnection.prototype.query = function query(ubq, allowBuffer){
var me = this, deferred;
if (allowBuffer===undefined || allowBuffer === false || !BUFFERED_DELAY){
return me.post('ubql', [ubq]).then(function(response){
return response.data[0];
});
} else {
deferred = Q.defer();
if (!me._bufferTimeoutPromise){
me._bufferTimeoutPromise = Q.delay(BUFFERED_DELAY).then(me.processBuffer.bind(me));
}
me._bufferedRequests.push({request: ubq, deferred: deferred});
return deferred.promise;
}
};
/**
* @deprecated Since UB 1.11 use `query` method
* @private
*/
UBConnection.prototype.run = function run(ubRequest, allowBuffer){
var me = this, deferred;
if (allowBuffer===undefined || allowBuffer === false || !BUFFERED_DELAY){
return me.post('ubql', [ubRequest]).then(function(response){
return response.data[0];
});
} else {
deferred = Q.defer();
if (!me._bufferTimeoutPromise){
me._bufferTimeoutPromise = Q.delay(BUFFERED_DELAY).then(me.processBuffer.bind(me));
}
me._bufferedRequests.push({request: ubRequest, deferred: deferred});
return deferred.promise;
}
};
/**
* Convert raw server response data to javaScript data according to attribute types.
* Called by {@link UBConnection#select}
* Currently only Data/DateTime & boolean conversion done
* If resultLock present - resultLock.lockTime also converted
*
* // convert all string representation of date/dateTime to Date object, integer representation of bool to Boolean
* return me.query({entity: 'my_entity', method: 'select'}, true)
* .then(me.convertResponseDataToJsTypes.bind(me));
*
* @method
* @param serverResponse
* @returns {*}
*/
UBConnection.prototype.convertResponseDataToJsTypes = function(serverResponse){
var convertRules, rulesLen, dataLen, data, d, r, column;
if (serverResponse.entity && // fieldList && serverResponse.fieldList
serverResponse.resultData &&
!serverResponse.resultData.notModified &&
serverResponse.resultData.fields &&
serverResponse.resultData.data && serverResponse.resultData.data.length)
{
convertRules = this.domain.get(serverResponse.entity).getConvertRules(serverResponse.resultData.fields);
rulesLen = convertRules.length;
data = serverResponse.resultData.data;
dataLen = data.length;
if (rulesLen) {
for (d=0; d<dataLen; d++){
for(r=0; r<rulesLen; r++){
column = convertRules[r].index;
data[d][column] = convertRules[r].convertFn(data[d][column]);
}
}
}
}
if (serverResponse.resultLock && serverResponse.resultLock.lockTime){
serverResponse.resultLock.lockTime = UB.iso8601Parse(serverResponse.resultLock.lockTime);
}
return serverResponse;
};
/**
* Call a {@link LocalDataStore#doFilterAndSort} - see a parameters there
* @protected
* @param {TubCachedData} cachedData
* @param {TubSelectRequest} ubRequest
* @returns {Object}
*/
UBConnection.prototype.doFilterAndSort = function (cachedData, ubRequest){
return LocalDataStore.doFilterAndSort(cachedData, ubRequest);
};
/**
* Promise of running UBQL command with `addNew` method (asynchronously).
* Two difference from {@link UBConnection.query}:
*
* - ubRequest.method set to 'addnew'
* - requests is always buffered in the 20ms period into one ubql call
* - `Date` & 'DateTime' entity attributes are converted from ISO8601 text representation to javaScript Date object
*
* Example:
*
* $App.connection.addNew({entity: 'uba_user', fieldList: ['*']}).done(UB.logDebug);
*
* @param {Object} serverRequest Request to execute
* @param {String} serverRequest.entity Entity to execute the method
* @param {String} [serverRequest.method] Method of entity to executed. Default to 'select'
* @param {Array.<string>} serverRequest.fieldList
* @param {Object} [serverRequest.execParams]
* @param {Object} [serverRequest.options]
* @param {String} [serverRequest.lockType]
* @param {Boolean} [serverRequest.alsNeed]
* @returns {Promise}
*/
UBConnection.prototype.addNew = function(serverRequest){
var
me = this;
serverRequest.method = 'addnew';
return me.query(serverRequest, true)
.then(me.convertResponseDataToJsTypes.bind(me));
};
/**
* Called in update/insert/delete methods and if request entity is cached - clear cacse
* @private
* @param serverResponse
* @return {Promise} Promise resolved to serverResponse
*/
UBConnection.prototype.invalidateCache = function(serverResponse){
var me = this,
cacheType = me.domain.get(serverResponse.entity).cacheType;
if (cacheType === UBCache.cacheTypes.none) {
return serverResponse;
}
return me.cacheOccurrenceRefresh(serverResponse.entity, cacheType).then(function(){
return serverResponse;
});
};
/**
* Promise of running UBQL command with `update` method (asynchronously).
* Difference from {@link UBConnection.query}:
*
* - ubRequest.method set to 'update'
* - `Date` & 'DateTime' entity attributes are converted from ISO8601 text representation to javaScript Date object
* - if necessary it will clear cache
*
* Example:
*
* $App.connection.update({entity: 'uba_user', fieldList: ['ID','name'], execParams: {ID: 1, name:'newName'}}).done(UB.logDebug);
*
* @param {Object} serverRequest Request to execute
* @param {String} serverRequest.entity Entity to execute the method
* @param {String} [serverRequest.method] Method of entity to executed. Default to 'update'
* @param {Array.<string>} serverRequest.fieldList
* @param {Object} [serverRequest.execParams]
* @param {Object} [serverRequest.options]
* @param {String} [serverRequest.lockType]
* @param {Boolean} [serverRequest.alsNeed]
* @param {Boolean} [allowBuffer] Default - false. Allow several "in the same time" request to be buffered to one transaction.
* @returns {Promise}
*/
UBConnection.prototype.update = function(serverRequest, allowBuffer){
var
me = this;
serverRequest.method = serverRequest.method || 'update';
return me.query(serverRequest, allowBuffer)
.then(me.convertResponseDataToJsTypes.bind(me))
.then(me.invalidateCache.bind(me));
};
/**
* Promise of running UnityBase UBQL command with `insert` method (asynchronously).
* Difference from {@link UBConnection.query}:
*
* - ubRequest.method set to 'insert'
* - `Date` & 'DateTime' entity attributes are converted from ISO8601 text representation to javaScript Date object
* - if necessary it will clear cache
*
* @param {Object} serverRequest Request to execute
* @param {String} serverRequest.entity Entity to execute the method
* @param {String} [serverRequest.method] Method of entity to executed. Default to 'insert'
* @param {Array.<string>} serverRequest.fieldList
* @param {Object} [serverRequest.execParams]
* @param {Object} [serverRequest.options]
* @param {String} [serverRequest.lockType]
* @param {Boolean} [serverRequest.alsNeed]
*
* @param {Boolean} [allowBuffer] Default - false. Allow several "in the same time" request to be buffered to one transaction.
*
* @method
* @returns {Promise}
*
* Example:
*
* $App.connection.insert({entity: 'uba_user', fieldList: ['ID','name'], execParams: {ID: 1, name:'newName'}).done(UB.logDebug);
*
*/
UBConnection.prototype.insert = function(serverRequest, allowBuffer){
var
me = this;
serverRequest.method = serverRequest.method || 'insert';
return me.query(serverRequest, allowBuffer)
.then(me.convertResponseDataToJsTypes.bind(me))
.then(me.invalidateCache.bind(me));
};
/**
* Promise of running UBQL command with delete method (asynchronously).
* Difference from {@link UBConnection.query}:
*
* - ubRequest.method set to 'delete' by default
* - if necessary it will clear cache
*
* @param {Object} serverRequest Request to execute
* @param {String} serverRequest.entity Entity to execute the method
* @param {String} [serverRequest.method] Method of entity to executed. Default to 'delete'
* @param {Object} [serverRequest.execParams]
* @param {Object} [serverRequest.options]
* @param {String} [serverRequest.lockType]
* @param {Boolean} [serverRequest.alsNeed]
*
* @param {Boolean} [allowBuffer] Default - false. Allow several "in the same time" request to be buffered to one transaction.
*
* @method
* @returns {Promise}
*
* Example:
*
* $App.connection.doDelete({entity: 'uba_user', fieldList: ['ID','name'], execParams: {ID: 1, name:'newName'}).done(UB.logDebug);
*
*/
UBConnection.prototype.doDelete = function(serverRequest, allowBuffer){
var
me = this;
serverRequest.method = serverRequest.method || 'delete';
return me.query(serverRequest, allowBuffer)
.then(me.invalidateCache.bind(me));
};
/**
* Promise of running UBQL (asynchronously).
* Two difference from {@link UBConnection.query}:
*
* - ubRequest.method by default set to 'select'
* - requests is always buffered in the 20ms period into one ubql call
* - `Date` & 'DateTime' entity attributes are converted from ISO8601 text representation to javaScript Date object
* - if request entity is cached - cache used
*
* @param {Object} serverRequest Request to execute
* @param {String} serverRequest.entity Entity to execute the method
* @param {String} [serverRequest.method] Method of entity to executed. Default to 'select'
* @param {Number} [serverRequest.ID] if passed - request bypass cache, where & order list is ignored. Can be used to load single record from server
* @param {Array.<string>} serverRequest.fieldList
* @param {Object} [serverRequest.whereList]
* @param {Object} [serverRequest.execParams]
* @param {Object} [serverRequest.options]
* @param {String} [serverRequest.lockType]
* @param {Boolean} [serverRequest.alsNeed]
* @param {Boolean} [serverRequest.__skipOptimisticLock] In case this parameter true and in the buffered
* @param {Boolean} [bypassCache=false] Do not use cache while request even if entity cached.
* If `__mip_disablecache: true` is passed in serverRequest cache is also disabled.
* @method
* @returns {Promise}
*
* Example:
*
* //retrieve users
* $App.connection.select({entity: 'uba_user', fieldList: ['*']}).done(UB.logDebug);
*
* //retrieve users and desktops and then both done - do something
* Q.all($App.connection.select({entity: 'uba_user', fieldList: ['ID', 'name']})
* $App.connection.select({entity: 'ubm_desktop', fieldList: ['ID', 'code']})
* ).done(UB.logDebug);
*/
UBConnection.prototype.select = function(serverRequest, bypassCache){
var me = this,
cacheTypes = UBCache.cacheTypes,
serverRequestWOLimits = {},
cacheType, dataPromise, cKey, cacheStoreName,
/**
*
* @param {Object} serverResponse
* @param {Object} serverResponse.resultData miscmimi
* @param {Boolean} [serverResponse.resultData.notModified]
* @param {String} storeName
* @returns {*}
*/
processVersionedResponse = function(serverResponse, storeName){
if (serverResponse.resultData.notModified){
// in case we refresh cachedSessionEntityRequested or just after login - put version to cachedSessionEntityRequested
me.cachedSessionEntityRequested[cKey] = serverResponse.version;
return me.cache.get(cKey, storeName);
} else {
return me.cache.put([
{key: cKey+':v', value: serverResponse.version},
{key: cKey, value: serverResponse.resultData}
], storeName)
.then(function(){
me.cachedSessionEntityRequested[cKey] = serverResponse.version;
return serverResponse.resultData;
});
}
};
bypassCache = bypassCache || (serverRequest.__mip_disablecache === true),
cacheType = bypassCache || serverRequest.ID || serverRequest.bypassCache ? UBCache.cacheTypes.None :
me.domain.get(serverRequest.entity).cacheType;
if (!serverRequest.method){
serverRequest.method = 'select';
}
// if exist expression where ID = ... bypass cache
// if (idInWhere(serverRequest.whereList)){
// cacheType = cacheTypes.None;
// }
if (cacheType === cacheTypes.None){ //where & order is done by server side
dataPromise = me.query(serverRequest, true)
.then(me.convertResponseDataToJsTypes.bind(me))
.then(function(response){
var responseWithTotal = {}, resRowCount, opt, start, limit;
UB.apply(responseWithTotal, response);
if (response.__totalRecCount){
responseWithTotal.total = response.__totalRecCount;
} else if (response.resultData && response.resultData.data){
resRowCount = response.resultData.data.length;
if (!serverRequest.options){
responseWithTotal.total = resRowCount;
} else {
opt = serverRequest.options || {};
start = opt.start ? opt.start : 0;
limit = opt.limit || 0;
// in case we fetch less data then requested - this is last page and we know total
responseWithTotal.total = (resRowCount < limit) ? start + resRowCount : -1;
}
}
return responseWithTotal;
});
} else { //where & order is done by client side
//TODO check all filtered attribute is present in whereList - rewrite me.checkFieldList(operation);
cKey = me.cacheKeyCalculate(serverRequest.entity, serverRequest.fieldList);
cacheStoreName = (cacheType === cacheTypes.Session) ? UBCache.SESSION : UBCache.PERMANENT;
// retrieve data either from cache or from server
dataPromise = me.cache.get(cKey+':v', cacheStoreName).then(function(version){
var cachedPromise;
if (!version || // no data in cache or invalid version
// or must re-validate version
(version && cacheType === cacheTypes.Entity) ||
// or SessionEntity cached not this current cache version
(version && cacheType === cacheTypes.SessionEntity && me.cachedSessionEntityRequested[cKey] !== version)
)
{
UB.apply(serverRequestWOLimits, serverRequest);
delete serverRequestWOLimits.whereList;
delete serverRequestWOLimits.orderList;
delete serverRequestWOLimits.options;
serverRequestWOLimits.version = version ? version : '-1';
//noinspection JSCheckFunctionSignatures
cachedPromise = me.query(serverRequestWOLimits, true)
.then(me.convertResponseDataToJsTypes.bind(me))
.then(function(response){
return processVersionedResponse(response, cacheStoreName);
});
} else { // retrieve data from cache
cachedPromise = me.cache.get(cKey, cacheStoreName);
}
return cachedPromise;
}).then(function(cacheResponse){
//noinspection JSCheckFunctionSignatures
return me.doFilterAndSort(cacheResponse, serverRequest);
});
}
return dataPromise;
};
/**
* Alias to {@link LocalDataStore#selectResultToArrayOfObjects LocalDataStore.selectResultToArrayOfObjects}
*
* @param {{resultData: {data: Array.<Array>, fields: Array.<String>}}} selectResult
* @returns {Array.<*>}
* @private
* @deprecated Use LocalDataStore.selectResultToArrayOfObjects
*/
UBConnection.selectResultToArrayOfObjects = LocalDataStore.selectResultToArrayOfObjects;
/**
* Execute numbers of ubRequest in one server request (one transaction)
*
* $App.connection.runTrans([
* { entity: 'my_entity', method: 'update', ID: 1, execParams: {code: 'newCode'} },
* { entity: 'my_entity', method: 'update', ID: 2, execParams: {code: 'newCodeforElementWithID=2'} },
* ]).done(UB.logDebug);
*
* @method
* @param {Array.<ubRequest>} ubRequestArray
* @returns {Promise} Resolved to response.data
*/
UBConnection.prototype.runTrans = function run(ubRequestArray){
var me = this;
return me.post('ubql', ubRequestArray).then(function(response){
return response.data;
});
};
/**
* Retrieve content of `document` type attribute field from server. Usage samples:
*
* //Retrieve content of document as string using GET
* $App.connection.getDocument({
* entity:'ubm_form',
* attribute: 'formDef',
* ID: 100000232003
* }).done(function(result){console.log(typeof result)}); // string
*
* //The same, but using POST for bypass cache
* $App.connection.getDocument({
* entity:'ubm_form',
* attribute: 'formDef',
* ID: 100000232003
* }, {
* bypassCache: true
* }).done(function(result){console.log(typeof result)}); // string
*
*
* //Retrieve content of document as ArrayBuffer and bypass cache
* $App.connection.getDocument({
* entity:'ubm_form',
* attribute: 'formDef',
* ID: 100000232003
* }, {
* bypassCache: true, resultIsBinary: true
* }).done(function(result){
* console.log('Result is', typeof result, 'of length' , result.byteLength, 'bytes'); //output: Result is object of length 2741 bytes
* });
*
* @param {Object} params
* @param {String} params.entity Code of entity to retrieve from
* @param {String} params.attribute `document` type attribute code
* @param {Number} params.id Instance ID
* @param {String} [params.forceMime] If passed and server support transformation from source MIME type to `forceMime` server perform transformation and return documenRt representation in the passed MIME
* @param {Number} [params.revision] Optional revision of the documnet (if supported by server-side store configuration). Default is current revision.
* @param {String} [params.fileName] ????
* @param {Boolean} [params.isDirty=false] Optional ability to retrieve document in **dirty** state
* @param {String} [params.store] ????
*
* @param {Object} [options] Additional request options
* @param {Boolean} [options.resultIsBinary=false] if true - return document content as arrayBuffer
* @param {Boolean} [options.bypassCache] HTTP POST verb will be used instead of GET for bypass browser cache
* @returns {Promise} Resolved to document content (either ArrayBuffer in case options.resultIsBinary===true or text/json)
*/
UBConnection.prototype.getDocument = function(params, options){
var
opt = _.defaults({}, options),
allowedParams = ['entity', 'attribute', 'ID', 'id', 'isDirty', 'forceMime', 'fileName', 'store', 'revision'],
reqParams = {
url: 'getDocument',
method: opt.bypassCache ? 'POST' : 'GET'
};
if (options && options.resultIsBinary){
reqParams.responseType = 'arraybuffer';
}
if (opt.bypassCache){
reqParams.data = _.clone(params);
Object.keys(reqParams.data).forEach(function(key){
if (allowedParams.indexOf(key) === -1){
delete reqParams.data[key];
UB.logDebug('invalid parameter "' + key + '" passed to getDocument request');
}
});
} else {
reqParams.params = params;
}
return this.xhr(reqParams)
.then(function(response){
return response.data;
});
};
/**
* Alias to {@link UBSession.hexa8 UBSession.hexa8}
* @private
* @deprecated since 1.8 use UBSession.prototype.hexa8 instead
*/
UBConnection.prototype.hexa8 = UBSession.prototype.hexa8;
/**
* Alias to {@link UBSession.hexa8 UBSession.crc32}
* @private
* @deprecated since 1.8 use UBSession.prototype.crc32 instead
*/
UBConnection.prototype.crc32 = UBSession.prototype.crc32;
/**
* Log out user from server
*/
UBConnection.prototype.logout = function(){
var me = this;
if ( me.isAuthorized() ){
return me.post('logout', {})
.then(Q.delay(function(){
if (me._pki) {
me._pki.closePK();
}
}, 20));
} else {
return Q.resolve(true);
}
};
/**
* Known server-side error codes
* @enum
* @private
*/
UBConnection.prototype.serverErrorCodes = {
1: "ubErrNotImplementedErrnum",
2: "ubErrRollbackedErrnum",
3: "ubErrNotExecutedErrnum",
4: "ubErrInvaliddataforrunmethod",
5: "ubErrInvaliddataforrunmethodlist",
6: "ubErrNoMethodParameter",
7: "ubErrMethodNotExist",
8: "ubErrElsAccessDeny",
9: "ubErrElsInvalidUserOrPwd",
10: "ubErrElsNeedAuth",
11: "ubErrNoEntityParameter",
13: "ubErrNoSuchRecord",
14: "ubErrInvalidDocpropFldContent",
15: "ubErrEntityNotExist",
16: "ubErrAttributeNotExist",
17: "ubErrNotexistEntitymethod",
18: "ubErrInvalidSetdocData",
19: "ubErrSoftlockExist",
20: "ubErrNoErrorDescription",
21: "ubErrUnknownStore",
22: "ubErrObjdatasrcempty",
23: "ubErrObjattrexprbodyempty",
24: "ubErrNecessaryfieldNotExist",
25: "ubErrRecordmodified",
26: "ubErrNotexistnecessparam",
27: "ubErrNotexistfieldlist",
28: "ubErrUpdaterecnotfound",
29: "ubErrNecessaryparamnotexist",
30: "ubErrInvalidstoredirs",
31: "ubErrNofileinstore",
32: "ubErrAppnotsupportconnection",
33: "ubErrAppnotsupportstore",
34: "ubErrDeleterecnotfound",
35: "ubErrNotfoundlinkentity",
36: "ubErrEntitynotcontainmixinaslink",
37: "ubErrEssnotinherfromessaslink",
38: "ubErrInstancedatanameisreadonly",
39: "ubErrManyrecordsforsoftlock",
40: "ubErrNotfoundidentfieldsl",
41: "ubErrInvalidlocktypevalue",
42: "ubErrLockedbyanotheruser",
43: "ubErrInvalidwherelistinparams",
44: "ubErrRecnotlocked",
45: "ubErrManyrecordsforchecksign",
46: "ubErrNotfoundparamnotrootlevel",
47: "ubErrCantcreatedirlogmsg",
48: "ubErrCantcreatedirclientmsg",
49: "ubErrConnectionNotExist",
50: "ubErrDirectUnityModification",
51: "ubErrCantdelrecthisvalueusedinassocrec",
52: "ubErrAssocattrnotfound",
53: "ubErrAttrassociationtoentityisempty",
54: "ubErrNotfoundconforentityinapp",
55: "ubErrNewversionrecnotfound",
56: "ubErrElsAccessDenyEntity",
57: "ubErrAlsAccessDenyEntityattr",
58: "ubErrDatastoreEmptyentity",
//59: "ubErrCustomerror"
67: "ubErrTheServerHasExceededMaximumNumberOfConnections",
69: "ubErrFtsForAppDisabled",
72: "ubErrElsPwdIsExpired"
};
/**
* Return server-side error message by error number
* @param {Number} errorNum
* @return {String}
*/
UBConnection.prototype.serverErrorByCode = function(errorNum) {
return this.serverErrorCodes[errorNum];
};
module.exports = UBConnection;