/* global window, navigator, Blob, FileReader, document, XMLHttpRequest,setTimeout,clearTimeout */
/**
* The UB namespace (global object) encapsulates all classes, singletons, and
* utility methods provided by UnityBase for build a transport layer for a Web applications.
*
* The main entry point for most operation is {@link UBConnection UBConnection} for communication with UnityBase server.
*
* @namespace UB
* @author pavel.mash
*/
var UB = {};
//<editor-fold desc="Service functions block">
var Q = require('./libs/q'),
/*last request content & date we send to server. We use it to prevent a reiteration requests with the same content */
__lastRequestData,
__lastRequestTime = new Date().getTime(),
MODEL_RE = new RegExp('models/(.+?)/'),// speculative search. w/o ? found maximum string length
FORMAT_RE = /\{(\d+)}/g,
__i18n = {
monkeyRequestsDetected: 'Your request has been processed, but we found that it is repeated several times. Maybe you key fuse?'
};
/**
* Copies all the properties of one or several objectsFrom to the specified objectTo.
* Non-simple type copied by reference!
* @param {Object} objectTo The receiver of the properties
* @param {...Object} objectsFrom The source(s) of the properties
* @return {Object} returns objectTo
*/
UB.apply = function(objectTo, objectsFrom) {
Array.prototype.forEach.call(arguments, function(obj) {
if (obj && obj !== objectTo) {
Object.keys(obj).forEach(function(key) {
objectTo[key] = obj[key];
});
}
});
return objectTo;
};
/**
* Allows you to define a tokenized string and pass an arbitrary number of arguments to replace the tokens. Each
* token must be unique, and must increment in the format {0}, {1}, etc. Example usage:
*
* var s = UB.format('{1}/ext-lang-{0}.js', 'en', 'locale');
* // s now contains the string: ''locale/ext-lang-en.js''
*
* @param {String} stringToFormat The string to be formatted.
* @param {...*} values The values to replace tokens `{0}`, `{1}`, etc in order.
* @return {String} The formatted string.
*/
UB.format = function(stringToFormat, values) {
var args = _.toArray(arguments).slice(1);
return stringToFormat.replace(FORMAT_RE, function(m, i) {
return args[i];
});
};
/**
* Creates namespaces to be used for scoping variables and classes so that they are not global.
* @example
* UB.ns('DOC.Report');
*
* DOC.Report.myReport = function() { ... };
*
* @method
* @param {String} namespacePath
* @return {Object} The namespace object.
*/
UB.ns = function(namespacePath){
var root = window,
parts, part, j, subLn;
parts = namespacePath.split('.');
for (j = 0, subLn = parts.length; j < subLn; j++) {
part = parts[j];
if (!root[part]) {
root[part] = {};
}
root = root[part];
}
return root;
};
/**
* Convert UnityBase server dateTime response to Date object
* @param value
* @returns {Date}
*/
UB.iso8601Parse = function(value) {
return value ? new Date(value): null;
};
/**
* Convert UnityBase server date response to Date object.
* date response is a day with 00 time (2015-07-17T00:00Z), to get a real date we must add current timezone shift
* @param value
* @returns {Date}
*/
UB.iso8601ParseAsDate = function(value) {
var res = value ? new Date(value): null;
if (res) {
res.setTime(res.getTime() + res.getTimezoneOffset() * 60 * 1000);
}
return res;
};
/**
* Convert UnityBase server Boolean response to Boolean (0 = false & 1 = trhe)
* @param v Value to convert
* @returns {Boolean|null}
*/
UB.booleanParse = function(v) {
if (typeof v === 'boolean') {
return v;
}
if ((v === undefined || v === null || v === '')) {
return null;
}
return v === 1;
};
/**
* Return locale-specific resource from it identifier.
* localeString must be previously defideb dy call to {@link UB#i18nExtend}
* @method
* @param {String} localeString
* @returns {*}
*/
UB.i18n = function(localeString){
return __i18n[localeString] || localeString;
};
/**
* Merge localizationObject to UB.i18n. Usually called form modelPublic/locale/lang-*.js scripts
* @method
* @param {Object} localizationObject
*/
UB.i18nExtend = function(localizationObject){
_.merge(__i18n, localizationObject);
};
/** @type {String} */
UB.userAgent = navigator.userAgent.toLowerCase();
/** @type {Boolean} */
UB.isChrome = /\bchrome\b/.test(UB.userAgent);
/** @type {Boolean} */
UB.isWebKit = /webkit/.test(UB.userAgent);
/** @type {Boolean} */
UB.isGecko = !UB.isWebKit && /gecko/.test(UB.userAgent);
/** @type {Boolean} */
UB.isOpera = /opr|opera/.test(UB.userAgent);
/** @type {Boolean} */
UB.isMac = /macintosh|mac os x/.test(UB.userAgent);
/** @type {Boolean} */
UB.isSecureBrowser = /\belectron\b/.test(UB.userAgent);
//</editor-fold>
UB.consts = {
};
// if (UB.isSecureBrowser) {
// var electron = require('electron');
// electron.ipcRenderer.on('SETTINGS', function(event, message){
// UB.secureSettings = JSON.parse(message);
// });
// var session = electron.remote.session;
// session.defaultSession.on('will-download', function (event, item, webContents) {
// var fileName, savePath;
// var path = require('path');
// savePath = path.join(UB.secureSettings.downloadFolder, item.getFilename());
// item.setSavePath(savePath);
// //event.preventDefault();
// //'URL', item.getURL(), 'MIME', item.getMimeType(), 'hasUserGesture', item.hasUserGesture(), 'getFileName', item.getFilename(), 'contentDisposition', item.getContentDisposition());
// item.once('updated', function(event, state){
// fileName = item.getFilename();
// savePath = item.getSavePath();
// });
// item.once('done', function (event, state) {
// if (state === 'completed') {
// $App.connection.query({
// entity: 'uba_audit',
// method: 'secureBrowserEvent',
// action: 'DOWNLOAD',
// reason: savePath
// }).then(function(){
// return $App.dialogInfo('Файл збережено до ' + savePath);
// }).done();
// } else {
// $App.dialogError('Завантаження відмінено');
// }
// });
// });
// }
var BASE64STRING = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
var BASE64ARR = [];
(function(){
for(var i = 0, l = BASE64STRING.length - 1; i < l; i++){
BASE64ARR.push(BASE64STRING[i]);
}
})();
/**
* Transform ArrayBuffer to base64 string
* @deprecated Since 1.9.6 use x10 times faster async {@link UB.base64FromAny} instead
* @method
* @private
* @param {ArrayBuffer} arraybuffer
* @returns {String}
*/
UB.base64fromArrayBuffer = function(arraybuffer) {
//MPV this is fastest synch implementation possible. TODO - check 4-bytes table?
//console.time('b641'); for(var t=0; t<100; t++){var v = new Uint8Array(10000); for(k=0; k<10000; k++){v[k]=k%100}; var b641 = UB.base64fromArrayBuffer4(v);} console.timeEnd('b641');
var bytes = new Uint8Array(arraybuffer),
i, len = bytes.buffer.byteLength, base64 = '';
for (i = 0; i < len; i+=3) {
base64 += BASE64STRING[bytes[i] >> 2];
base64 += BASE64STRING[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
base64 += BASE64STRING[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
base64 += BASE64STRING[bytes[i + 2] & 63];
}
if ((len % 3) === 2) {
base64 = base64.substring(0, base64.length - 1) + "=";
} else if (len % 3 === 1) {
base64 = base64.substring(0, base64.length - 2) + "==";
}
return base64;
};
/**
* Fast async transformation of data to base64 string
* @method
* @param {File|ArrayBuffer|String|Blob|Array} data
* @returns {Promise} resolved to data converted to base64 string
*/
UB.base64FromAny = function(data) {
var
reader = new FileReader(),
deferred = Q.defer(),
blob;
blob = (data instanceof Blob) ? data : new Blob([data]);
reader.addEventListener('loadend', function () {
deferred.resolve(reader.result.split(',', 2)[1]); //remove data:....;base64, from the beginning of string
});
reader.addEventListener('error', function (event) {
deferred.reject(event);
});
reader.readAsDataURL(blob);
return deferred.promise;
};
/**
* Convert dataBlob contain PDF document to base64 string
* @deprecated Since 1.9.6 use {@link UB.base64FromAny} instead
* @private
* @param {Blob} pdfDataBlob
* @returns {Promise} Promise sesolved to base64 representation of dataBlob
*/
UB.base64fromPdfDataBlob = function(pdfDataBlob){
return UB.base64FromAny(pdfDataBlob);
};
var BASE64DECODELOOKUP = new Uint8Array( 256 );
(function(){
for( var i = 0, l=BASE64STRING.length; i < l; i ++ ) {
BASE64DECODELOOKUP[BASE64STRING[i].charCodeAt(0)] = i;
}
})();
/**
* Convert base54 encoded string to decoded array buffer
* @param {String} base64
* @returns {ArrayBuffer}
*/
UB.base64toArrayBuffer = function(base64) {
var bufferLength = base64.length * 0.75,
len = base64.length, i, p = 0,
encoded1, encoded2, encoded3, encoded4;
if (base64[base64.length - 1] === "=") {
bufferLength--;
if (base64[base64.length - 2] === "=") {
bufferLength--;
}
}
var arrayBuffer = new ArrayBuffer(bufferLength),
bytes = new Uint8Array(arrayBuffer);
for (i = 0; i < len; i+=4) {
encoded1 = BASE64DECODELOOKUP[base64.charCodeAt(i)];
encoded2 = BASE64DECODELOOKUP[base64.charCodeAt(i+1)];
encoded3 = BASE64DECODELOOKUP[base64.charCodeAt(i+2)];
encoded4 = BASE64DECODELOOKUP[base64.charCodeAt(i+3)];
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
}
return arrayBuffer;
};
/** @property
* index*.html template can define models versions inside this property.
* See implementation inside /models/UBM/index.html.js
*
* If so, {@link UB.addResourceVersion UB.addResourceVersion} will add
* version parameter to scripts inside models.
*
* Also used in {@link UB#require UB.require}
* @private
*/
UB.__ubVersion = window.__ubVersion;
/**
* @property {Object} appConfig UnityBase application parameter.
* All this parameters can be configured in ubConfig.uiSettings.adminUI section of application configuration file.
*
* @property {String} appConfig.applicationName Name of current application. This text shown in the left side of main window toolbar in Ext-based web client
* @property {String} appConfig.loginWindowTopLogoURL
* @property {String} appConfig.loginWindowBottomLogoURL
* @property {String} appConfig.defaultLang Default application language
* @property {Array.<String>} appConfig.supportedLanguages Array of all languages supported by application.
* @property {String} appConfig.defaultPasswordForDebugOnly Fill password on login window. Set this parameter for debug purpose only!
*
* @property {Number} appConfig.comboPageSize Page size of UBCombobox drop-down (in case !disablePaging)
* @property {Number} appConfig.maxMainWindowTabOpened How many tab user can open in the same time in the main workspace
*
* @property {Number} appConfig.storeDefaultPageSize Default page size of UnityBase store
* @property {Number} appConfig.gridHeightDefault Default height of grid in form
* @property {Number} appConfig.gridWidthDefault Default width of grid in form
* @property {Number} appConfig.gridParentChangeEventTimeout Timeout for fire parentchange event
* @property {Number} appConfig.gridDefaultDetailViewHeight Default height of detail grid and preview form inside master grid
*
* @property {Number} appConfig.formMinHeight Minimum form height
* @property {Number} appConfig.formMinWidth Minimum form width
* @property {Number} appConfig.formDefaultAutoFormWidth Default width of auto-generated form
* @property {Number} appConfig.formSaveMaskDelay How long (in ms) wait before mask form while save action call. Usually save if quick operation and we do not need mask form at all
* @property {Number} appConfig.scanRecognizeProgressInterval Callback call interval while do scan recognition using UBDesktopService extension
* @property {String} appConfig.browserExtensionNMHostAppKey Native messages plugin application key
*/
UB.appConfig = {
applicationName: 'UnityBase',
loginWindowTopLogoURL: '', //'images/UBLogo128.png',
loginWindowBottomLogoURL: '',
themeName: 'UBtheme',
userDbVersion: null,
defaultLang: 'en',
supportedLanguages: ['en'],
defaultPasswordForDebugOnly: '',
comboPageSize: 30,
maxMainWindowTabOpened: 10,
storeDefaultPageSize: 100,
gridHeightDefault: 400,
gridWidthDefault: 600,
gridParentChangeEventTimeout: 200,
gridDefaultDetailViewHeight: 150,
formMinHeight: 100,
formMinWidth: 300,
formDefaultAutoFormWidth: 300,
formSaveMaskDelay: 100,
scanRecognizeProgressInterval: 1000,
maxSearchLength: 62,
browserExtensionNMHostAppKey: 'com.inbase.ubmessagehost'
};
/**
* Log message to console (if console available)
* @method
* @param {...*} msg
*/
UB.log = log;
function log(msg){
if (console){
console.log.apply(console, arguments);
}
}
/**
* Log error message to console (if console available)
* @method
* @param {...*} msg
*/
UB.logError = logError;
function logError(msg){
if (console){
console.error.apply(console, arguments);
}
}
/**
* Log warning message to console (if console available)
* @method
* @param {...*} msg
*/
UB.logWarn = logWarn;
function logWarn(msg){
if (console){
console.warn.apply(console, arguments);
}
}
/**
* Log debug message to console.
* Since it binded to console, can also be used to debug Promise resolving in this way:
*
* UB.get('timeStamp').done(UB.logDebug);
*
* @method
* @param {...*} msg
*/
UB.logDebug = console.info.bind(console);
/**
* The onError callback
* @callback UB-onError
* @param {string} errorMessage
* @param {Number} [errorNum]
*/
/**
* onError handler. If set - this one is call, otherwise - default handler will be called
* @type {UB-onError}
*/
UB.onError = null;
/** @method
* @param {String} errMsg
* @param {Number} [errCode]
* @type {Function}
*/
UB.doOnProcessError = doOnProcessError;
function doOnProcessError(errMsg, errCode){
if(UB.onError){
UB.onError(errMsg, errCode);
}else{
throw new Error(errMsg);
}
}
/**
* Handler, called by login method. This handler must show login window and call callback with parameter:
* { authSchema: authSchemaUserChoose, login: loginNameUserInput, pwd: passwordUserInput, callback: Function(result)}
* after user confirm Login.
* After do authentication UB call callback with object {success: Boolean, errMsg: String}
* Login window can use data from {@link UB#appConfig UB.appConfig} - it is initialized before login call
* @property {function(Function)}
*/
UB.onShowLoginWindow = null;
UB.doLogin = function doLogin(){
if (!UB.onShowLoginWindow){
throw new Error('no UB.onShowLoginWindow handler defined');
}
};
/**
* Search for resource version in the window.__ubVersion global const
* IF any, return 'ver=version' else ''
* @param {String} uri
* @returns {String}
*/
UB.getResourceVersion = function(uri){
var
modelName = MODEL_RE.test(uri) ? MODEL_RE.exec(uri)[1] : '_web';
if (UB.__ubVersion && UB.__ubVersion[modelName]){
return '?ubver=' + UB.__ubVersion[modelName];
} else {
return '';
}
};
/**
* Exec UB.getResourceVersion and if any - add ?ver to resource and return resource with ?ver
* @method
* @param {String} uri
* @returns {String} uri with added resource version
*/
UB.addResourceVersion = function(uri){
var
ver = UB.getResourceVersion(uri);
return ver ? uri + ver : uri;
};
//<editor-fold desc="Promise-based script injection">
var
__loadedScript = {},
__head = document.getElementsByTagName("head")[0];
/**
* Inject external script or css to DOM and return a promise to be resolved when script is loaded.
*
* Implement single load mode (if script successfully loaded using {@link UB.inject UB.inject} it not loaded anymore.
*
* @example
*
//Load script.js:
UB.inject('jslibs/script.js').done();
//Load several script at once and error handling:
Q.all([UB.inject('jslibs/script.js'), UB.inject('script2.js')])
.fail(function(err){
console.log('Oh! error occurred: ' + err) ;
});
//Load one script and then load other
UB.inject('jslibs/js_beautify.js')
.then(function(){
console.log('first script loaded. Continue to load second');
return UB.inject('jslibs/js_beautify1.js');
}).done();
//Load couple of resources:
Q.all([UB.inject('css/first.css'), UB.inject('css/second.css')]).done();
* @method
* @param {String} url either *js* or *css* resource to load
* @param {String} [charset]
* @return {Promise}
*/
UB.inject = function( url, charset ) {
var
elm = null,
dfd = Q.defer(),
injectScript = function (url, resultHandler, failHandler) {
var
isCSS = /\.css(?:\?|$)/.test(url);
if (isCSS){
elm = document.createElement('link');
elm.rel = 'stylesheet';
elm.async = true;
} else {
elm = document.createElement('script');
elm.type = 'text/javascript';
if (charset){
elm.charset = charset;
}
elm.async = true;
}
elm.onerror = failHandler;
if ('addEventListener' in elm) {
elm.onload = resultHandler;
} else if ('readyState' in elm) { // for <IE9 Compatability
elm.onreadystatechange = function () {
if (this.readyState === 'loaded' || this.readyState === 'complete') {
resultHandler();
}
};
} else {
elm.onload = resultHandler;
}
__head.appendChild(elm);
// src must be set AFTER onload && onerror && appendChild
if (isCSS){
elm.href = url;
} else {
elm.src = url;
}
return elm;
},
onLoadOK = function() {
elm.onerror = elm.onload = elm.onreadystatechange = null;
setTimeout(function(){ // script must evaluate first
dfd.resolve();
// Remove the script (do not remove CSS) ???
if ( elm.parentNode && !elm.rel) {
elm.parentNode.removeChild( elm );
}
}, 0);
},
onLoadFail = function(oError) {
var
reason = 'Required ' + (oError.target.href || oError.target.src) + ' is not accessible';
delete __loadedScript[url];
elm.onerror = elm.onload = elm.onreadystatechange = null;
dfd.reject(new Error(reason));
};
if (__loadedScript[url]){
dfd.resolve(__loadedScript[url]);
} else {
// Create and inject script tag at end of DOM body and load the external script
// attach event listeners that will trigger the Deferred.
__loadedScript[url] = dfd;
injectScript( UB.addResourceVersion(url), onLoadOK, onLoadFail );
}
return dfd.promise;
};
//</editor-fold>
//<editor-fold desc="Promise-based XHR">
function lowercase(str) {
return (str || '').toLowerCase();
}
function parseHeaders(headers) {
var parsed = {}, key, val, i;
if (!headers) {
return parsed;
}
headers.split('\n').forEach(function(line) {
i = line.indexOf(':');
key = lowercase(line.substr(0, i).trim());
val = line.substr(i + 1).trim();
if (key) {
if (parsed[key]) {
parsed[key] += ', ' + val;
} else {
parsed[key] = val;
}
}
});
return parsed;
}
function headersGetter(headers) {
var headersObj = typeof headers === 'object' ? headers : undefined;
return function(name) {
if (!headersObj) {
headersObj = parseHeaders(headers);
}
if (name) {
return headersObj[lowercase(name)];
}
return headersObj;
};
}
function transformData(data, headers, fns) {
if (typeof fns === 'function') {
return fns(data, headers);
}
fns.forEach(function(fn) {
data = fn(data, headers);
});
return data;
}
function transformDataPromise(data, headers, fns) {
var rpromise = Q.resolve(data);
if (typeof fns === 'function') {
return rpromise.then(function(rdata){
return fns(data, headers);
});
}
fns.forEach(function(fn) {
rpromise = rpromise.then(function(rdata){
return fn(data, headers);
});
});
return rpromise;
}
function isSuccess(status) {
return 200 <= status && status < 300;
}
function forEach(obj, iterator, context) {
var keys = Object.keys(obj);
keys.forEach(function(key) {
iterator.call(context, obj[key], key);
});
return keys;
}
function forEachSorted(obj, iterator, context) {
var keys = Object.keys(obj).sort();
keys.forEach(function(key) {
iterator.call(context, obj[key], key);
});
return keys;
}
function buildUrl(url, params) {
if (!params) {
return url;
}
var parts = [];
forEachSorted(params, function(value, key) {
if (value == null) { // jshint ignore:line
return;
}
if (!Array.isArray(value)) {
value = [value];
}
value.forEach(function(v) {
if (typeof v === 'object') {
v = JSON.stringify(v);
}
parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(v));
});
});
return url + ((url.indexOf('?') === -1) ? '?' : '&') + parts.join('&');
}
/**
* Promise of perform an asynchronous HTTP request
* Returns a {@link Q promise} object with the
* standard Promise methods (<a href="https://github.com/kriskowal/q/wiki/Coming-from-jQuery#reference">reference</a>).
* The `then` method takes two arguments a success and an error callback which will be called with a
* response object. The arguments passed into these functions are destructured representation of the response object passed into the
* `then` method. The response object has these properties:
*
* - **data** – `{string|Object}` – The response body transformed with the transform
* functions. Default transform check response content-type is application/json and if so - convert data to Object
* - **status** – `{number}` – HTTP status code of the response.
* - **headers** – `{function([headerName])}` – Header getter function.
* - **config** – `{Object}` – The configuration object that was used to generate the request.
*
* @example
*
* //Get some data from server:
* UB.xhr({url: 'getAppInfo'}).done(function(resp) {
* console.log('this is appInfo: %o', resp.data)
* });
*
* //The same, but in more short form via {@link UB#get UB.get} shorthand:
* UB.get('getAppInfo').done(function(resp) {
* console.log('this is appInfo: %o', resp.data)
* });
*
* //Run POST method:
* UB.post('ubql', [
* {entity: 'uba_user', method: 'select', fieldList: ['*']}
* ]).then(function(resp) {
* console.log('success!');
* }, function(resp) {
* console.log('request failed with status' + resp.status);
* });
*
* //retrieve binary data as ArrayBuffer
* UB.get('downloads/cert/ACSK(old).cer', {responseType: 'arraybuffer'})
* .done(function(res){
* console.log('Got Arrray of %d length', res.data.byteLength);
* });
*
* @method
* @param {Object} requestConfig Object describing the request to be made and how it should be
* processed. The object has following properties:
* @param {String} requestConfig.url Absolute or relative URL of the resource that is being requested
* @param {String} [requestConfig.method] HTTP method (e.g. 'GET', 'POST', etc). Default is GET
* @param {Object.<string|Object>} [requestConfig.params] Map of strings or objects which will be turned
* to `?key1=value1&key2=value2` after the url. If the value is not a string, it will be JSONified
* @param {String|Object} [requestConfig.data] Data to be sent as the request message data
* @param {Object} [requestConfig.headers] Map of strings or functions which return strings representing
* HTTP headers to send to the server. If the return value of a function is null, the
* header will not be sent. Merged with {@link UB#xhrDefaults UB.xhrDefaults.headers}
* @param {function(data, function)|Array.<function(data, function)>} [requestConfig.transformRequest]
* Transform function or an array of such functions. The transform function takes the http
* request body and headers and returns its transformed (typically serialized) version.
* @param {function(data, function)|Array.<function(data, function)>} [requestConfig.transformResponse]
* Transform function or an array of such functions. The transform function takes the http
* response body and headers and returns its transformed (typically deserialized) version.
* @param {Number|Promise} [requestConfig.timeout] timeout in milliseconds, or {@link Q promise}
* that should abort the request when resolved. Default to {UB.xhrDefaults.timeout}
* @param {Boolean} [requestConfig.withCredentials] whether to to set the `withCredentials` flag on the
* XHR object. See <a href="https://developer.mozilla.org/en/http_access_control#section_5">requests with credentials</a>
* for more information.
* @param {String} [requestConfig.responseType] see <a href="https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType">responseType</a>.
*
* @returns {Promise}
*/
UB.xhr = function(requestConfig) {
var defaults = UB.xhrDefaults,
config = {
transformRequest: defaults.transformRequest,
transformResponse: defaults.transformResponse
},
mergeHeaders = function(config) {
var defHeaders = defaults.headers,
reqHeaders = UB.apply({}, config.headers),
defHeaderName, lowercaseDefHeaderName, reqHeaderName,
execHeaders = function(headers) {
forEach(headers, function(headerFn, header) {
if (typeof headerFn === 'function') {
var headerContent = headerFn();
if (headerContent) {
headers[header] = headerContent;
} else {
delete headers[header];
}
}
});
};
defHeaders = UB.apply({}, defHeaders.common, defHeaders[lowercase(config.method)]);
// execute if header value is function
execHeaders(defHeaders);
execHeaders(reqHeaders);
// using for-in instead of forEach to avoid unecessary iteration after header has been found
defaultHeadersIteration:
//noinspection JSUnfilteredForInLoop,JSHint
for (defHeaderName in defHeaders) {
//noinspection JSUnfilteredForInLoop
lowercaseDefHeaderName = lowercase(defHeaderName);
for (reqHeaderName in reqHeaders) {
//noinspection JSUnfilteredForInLoop
if (lowercase(reqHeaderName) === lowercaseDefHeaderName) {
continue defaultHeadersIteration;
}
}
//noinspection JSUnfilteredForInLoop
reqHeaders[defHeaderName] = defHeaders[defHeaderName];
}
return reqHeaders;
},
headers = mergeHeaders(requestConfig);
UB.apply(config, requestConfig);
config.headers = headers;
config.method = (config.method || 'GET').toUpperCase();
var transformResponse, serverRequest, promise;
transformResponse = function(response) {
return transformDataPromise(response.data, response.headers, config.transformResponse)
.then(function(trdData){
response.data = trdData;
return isSuccess(response.status) ? response : Q.reject(response);
});
};
serverRequest = function(config) {
headers = config.headers;
var reqData = transformData(config.data, headersGetter(headers), config.transformRequest);
var prevReqTime = __lastRequestTime;
__lastRequestTime = new Date().getTime();
// strip content-type if data is undefined
if (!config.data) {
forEach(headers, function(value, header) {
if (lowercase(header) === 'content-type') {
delete headers[header];
}
});
} else {
// prevent reiteration sending of the same request
// for example if HTML button on the form got a focus and `space` pressed
// in case button not disabled inside `onclick` handler we got a many-many same requests
if ((typeof reqData === 'string') && (__lastRequestData === reqData) && (__lastRequestTime - prevReqTime < 100)){
throw new UB.UBError('monkeyRequestsDetected');
} else {
__lastRequestData = reqData;
}
}
if (!config.withCredentials && defaults.withCredentials) {
config.withCredentials = defaults.withCredentials;
}
if (!config.timeout && defaults.timeout){
config.timeout = defaults.timeout;
}
// send request
return sendReq(config, reqData, headers).then(transformResponse, transformResponse);
};
promise = Q.when(config);
// build a promise chain with request interceptors first, then the request, and response interceptors
UB.interceptors.filter(function(interceptor) {
return !!interceptor.request || !!interceptor.requestError;
}).map(function(interceptor) {
return { success: interceptor.request, failure: interceptor.requestError };
})
.concat({ success: serverRequest })
.concat(UB.interceptors.filter(function(interceptor) {
return !!interceptor.response || !!interceptor.responseError;
}).map(function(interceptor) {
return { success: interceptor.response, failure: interceptor.responseError };
})
).forEach(function(then) {
promise = promise.then(then.success, then.failure);
});
return promise;
};
/**
* Allow Request reiteration, for example in case of request are repeated after re-auth
*/
UB.xhr.allowRequestReiteration = function(){
__lastRequestData = null;
};
var CONTENT_TYPE_APPLICATION_JSON = { 'Content-Type': 'application/json;charset=utf-8' };
/**
* The default HTTP parameters for {@link UB.xhr}
* @property {Object} xhrDefaults
* @property {Array<Function>} xhrDefaults.transformRequest request transformations
* @property {Array<Function>} xhrDefaults.transformResponse response transformations
* @property {Object} xhrDefaults.headers Default headers to apply to request (depending of method)
* @property {Number} xhrDefaults.timeout Default timeout to apply to request
*/
UB.xhrDefaults = {
transformRequest: [function(data) {
return !!data && typeof data === 'object' && data.toString() !== '[object File]' && data.toString() !== '[object ArrayBuffer]' ?
JSON.stringify(data) : data;
}],
transformResponse: [function(data, headers) {
if (typeof data === 'string' && (headers('content-type') || '').indexOf('json') >= 0) {
data = JSON.parse(data);
}
return data;
}],
headers: {
common: { 'Accept': 'application/json, text/plain, */*' },
post: CONTENT_TYPE_APPLICATION_JSON,
put: CONTENT_TYPE_APPLICATION_JSON,
patch: CONTENT_TYPE_APPLICATION_JSON
},
timeout: 120000
};
/**
* Interceptors array
* @type {Array.<Object>}
* @protected
*/
UB.interceptors = [];
/**
* Array of config objects for currently pending requests. This is primarily meant to be used for debugging purposes.
* @type {Array.<Object>}
* @protected
*/
UB.pendingRequests = [];
var XHR = XMLHttpRequest;
function sendReq(config, reqData, reqHeaders) {
var deferred = Q.defer(),
promise = deferred.promise,
url = buildUrl(config.url, config.params),
xhr = new XHR(),
aborted = -1,
status,
timeoutId;
UB.pendingRequests.push(config);
xhr.open(config.method, url, true);
forEach(reqHeaders /* MPV config.headers */, function(value, key) {
if (value) {
xhr.setRequestHeader(key, value);
}
});
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
var response, responseHeaders;
if (status !== aborted) {
responseHeaders = xhr.getAllResponseHeaders();
// responseText is the old-school way of retrieving response (supported by IE8 & 9)
// response/responseType properties were introduced in XHR Level2 spec (supported by IE10)
response = xhr.responseType ? xhr.response : xhr.responseText;
}
// cancel timeout and subsequent timeout promise resolution
if (timeoutId) {
clearTimeout(timeoutId);
}
status = status || xhr.status;
xhr = null;
// normalize status, including accounting for IE bug (http://bugs.jquery.com/ticket/1450)
status = Math.max(status === 1223 ? 204 : status, 0);
var idx = UB.pendingRequests.indexOf(config);
if (idx !== -1) {
UB.pendingRequests.splice(idx, 1);
}
(isSuccess(status) ? deferred.resolve : deferred.reject)({
data: response,
status: status,
headers: headersGetter(responseHeaders),
config: config
});
}
};
if (xhr.upload) {
xhr.upload.onprogress = function (progress) {
deferred.notify(progress);
};
} else {
xhr.onprogress = function (progress) { //TODO - do we need this?
deferred.notify(progress);
};
}
if (config.withCredentials) {
xhr.withCredentials = true;
}
if (config.responseType) {
xhr.responseType = config.responseType;
}
xhr.send(reqData || null);
if (config.timeout > 0) {
timeoutId = setTimeout(function() {
status = aborted;
if (xhr) {
xhr.abort();
}
}, config.timeout);
}
return promise;
}
/**
* Shortcut for {@link UB.xhr} to perform a `GET` request.
* @method
* @param {string} url Relative or absolute URL specifying the destination of the request
* @param {Object=} [config] Optional configuration object as in {@link UB#xhr UB.xhr}
* @returns {Promise} Future object
*/
UB.get = function(url, config) {
return UB.xhr(UB.apply(config || {}, {
method: 'GET',
url: url
}));
};
/**
* Shortcut for {@link UB.xhr} to perform a `DELETE` request.
* @method
* @param {string} url Relative or absolute URL specifying the destination of the request
* @param {Object=} [config] Optional configuration object as in {@link UB#xhr UB.xhr}
* @returns {Promise} Future object
*/
/**
* Shortcut for {@link UB.xhr} to perform a `HEAD` request.
* @method
* @param {string} url Relative or absolute URL specifying the destination of the request
* @param {Object=} [config] Optional configuration object as in {@link UB#xhr UB.xhr}
* @returns {Promise} Future object
*/
['delete', 'head'].forEach(function(name) {
UB[name] = function(url, config) {
return UB.xhr(UB.apply(config || {}, {
method: name,
url: url
}));
};
});
/**
* Shortcut for {@link UB.xhr} to perform a `POST` request.
* @method
* @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 UB.xhr}
* @returns {Promise} Future object
*/
UB.post = function(url, data, config) {
return UB.xhr(UB.apply(config || {}, {
method: 'POST',
url: url,
data: data
}));
};
/**
* Shortcut for {@link UB.xhr} to perform a `PUT` request.
* @method put
* @memberof UB
* @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 UB.xhr}
* @returns {Promise} Future object
*/
/**
* Shortcut for {@link UB.xhr} to perform a `PATCH` request.
* @method patch
* @memberof UB
* @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 UB.xhr}
* @returns {Promise} Future object
*/
['put', 'patch'].forEach(function(name) {
UB[name] = function(url, data, config) {
return UB.xhr(UB.apply(config || {}, {
method: name,
url: url,
data: data
}));
};
});
/**
* UnityBase client-side exception.
* Such exceptions are will not be showed as unknown error in {@link UB#showErrorWindow}
*
* message Can be either localized message or locale identifier - in this case UB#showErrorWindow translate message using {@link UB#i18n}
*
* @example
* throw new UB.UBError('lockedBy'); // will show message box "Record was locked by other user. It\'s read-only for you now"
*
* @param {String} message Message
* @param {String} [detail] Error details
* @param {Number} [code] Error code (for server-side errors)
* @extends {Error}
*/
UB.UBError = function UBError(message, detail, code) {
this.name = 'UBError';
this.detail = detail;
this.code = code;
this.message = message || 'UBError';
if (Error.captureStackTrace){
Error.captureStackTrace(this, UBError);
} else {
this.stack = (new Error()).stack;
}
};
UB.UBError.prototype = new Error();
UB.UBError.prototype.constructor = UB.UBError;
/**
* UnityBase still error. Global error handler does not show this error for user. Use it for still reject promise.
* @param {String} [message] Message
* @param {String} [detail] Error details
* @extends {Error}
*/
UB.UBAbortError = function UBAbortError(message, detail) {
this.name = 'UBAbortError';
this.detail = detail;
this.code = 'UBAbortError';
this.message = message || 'UBAbortError';
if (Error.captureStackTrace){
Error.captureStackTrace(this, UBAbortError);
} else {
this.stack = (new Error()).stack;
}
};
UB.UBAbortError.prototype = new Error();
UB.UBAbortError.prototype.constructor = UB.UBAbortError;
var reLetters = /[A-Za-zА-Яа-яЁёіІїЇґҐ]/,
reEn = /[A-Za-z]/,
reCaps = /[A-ZА-ЯЁІЇҐ]/;
UB.passwordKeyUpHandler = function(textfield){
var t, n, s = textfield.getValue() || "";
if (!s) {
textfield.removeCls('ub-pwd-keyboard-caps');
textfield.removeCls('ub-pwd-keyboard-en');
} else {
n = s.length;
t = s.substr(n - 1, 1);
if (reLetters.test(t)) {
if (reEn.test(t)) {
textfield.addClass('ub-pwd-keyboard-en');
} else {
textfield.removeCls('ub-pwd-keyboard-en');
}
if (reCaps.test(t)) {
textfield.addClass('ub-pwd-keyboard-caps');
} else {
textfield.removeCls('ub-pwd-keyboard-caps');
}
}
}
};
/**
* If we are in UnityBase server scripting this property is true, if in browser - undefined or false.
* Use it for check execution context in scripts, shared between client & server.
* @property {Boolean} isServer
* @readonly
*/
Object.defineProperty(UB, 'isServer', {enumerable: true, value: false} );
//</editor-fold>
module.exports = UB;