/**
* @class
* @classdesc Hits builder.
* @name BuildManager
* @param parent {object} Instance of the Tag used
* @public
*/
var BuildManager = function (parent) {
'use strict';
var self = this;
var MAXSIZE = 1500;
var _secureProtocol = 'https:';
/**
* Create an element of the "preQuery" object.
* @memberof BuildManager#
* @function
* @param param {string} name of a querystring parameter
* @param value {string} value of a querystring parameter
* @param truncate {boolean} true if the parameter can truncate
* @returns {object}
* @private
*/
var _makeSubQuery = function(param, value, truncate) {
var p = '&' + param + '=';
return {
param: p,
paramSize: p.length,
str: value,
strSize: value.length,
truncate: truncate
};
};
/**
* Create one hit with the first elements of the given dictionary and deletes them from it.
* @memberof BuildManager#
* @function
* @param subQueries {object} dictionary generated with the _preQuery method
* @param hitMaxSize {number}
* @returns {Array} :
* - index 0 : query string to put on a hit
* <br />- index 1 : dictionary remains unused, null if empty
* @private
*/
var _makeOneHit = function(subQueries, hitMaxSize) {
var hit = '';
var usedSpace = 0;
var hitDone = true;
for (var key in subQueries) {
if (subQueries.hasOwnProperty(key)) {
var subQuery = subQueries[key];
if (subQuery) {
hitDone = false;
var freeSpace = hitMaxSize - usedSpace;
if (subQuery.strSize + subQuery.paramSize < freeSpace) {
// le nouveau paramètre rentre dans le hit
hit += subQuery.param + subQuery.str;
usedSpace += subQuery.strSize + subQuery.paramSize;
subQueries[key] = undefined;
hitDone = true;
} else if (subQuery.truncate) {
// le nouveau paramètre ne rentre pas dans le hit mais il est tronquable
hit += subQuery.param + subQuery.str.substr(0, freeSpace);
subQueries[key].str = subQuery.str.substr(freeSpace, subQuery.strSize - 1);
subQueries[key].strSize = subQueries[key].str.length;
break;
} else {
// le nouveau paramètre ne rentre pas dans le hit ET il n'est pas tronquable, on force son découpage et on log une erreur en debug mode
if(subQuery.strSize + subQuery.paramSize > hitMaxSize){
parent.emit('Tracker:Hit:Build:Error',{
lvl:'ERROR',
msg:'Too long parameter "' + subQuery.param + '"',
details:{value: subQuery.str}
});
// make the key + value under the max length allowed
subQueries[key].str = subQueries[key].str.substr(0,hitMaxSize - subQuery.paramSize - 1);
subQueries[key].strSize = subQueries[key].str.length;
}
break;
}
} else {
hitDone = true;
}
}
}
return [hit, hitDone?null:subQueries];
};
/**
* List of querystring parameters which can be truncated
* @type {String[]}
* @private
*/
var multiHitable = ['ati','atc','pdtl','stc','dz'];
/**
* First step in the formating of the querystring parameters: make a dictionary paramName => object (see _makeSubQuery).
* <br />The resulting dictionary contains all parameters in string format, in the right order, ready to be truncate if they can, etc.
* @memberof BuildManager#
* @function
* @param buffer {object} Buffer part to process
* @param maxsize {number} Hit max size
* @returns {Array} :
* - index 0 : preProcessed parameters
* <br />- index 1 : sum of the parameter size
* <br />- index 2 : true if a parameter was too long and can't be truncated
* <br />- index 3 : if sizeError name of the offending parameter
* @private
*/
var _preQuery = function(buffer, maxsize) {
var subQueries = {};
var totalSize = 0;
var sizeError = false;
var keySizeError = undefined;
var lastKey, lastSubQuery;
for (var key in buffer) {
if (buffer.hasOwnProperty(key)) {
var subQueryValue = buffer[key]['value'];
//si le paramètre est une fonction on l'exécute pour obtenir une valeur
// qui passera pas les traitements suivants (transformation en string, encodage, etc.)
if (typeof subQueryValue === 'function') {
subQueryValue = subQueryValue();
}
if (subQueryValue instanceof Array) {
//si la valeur est un tableau on le transforme en chaînes séparés par un séparateur donné ou par ','
subQueryValue = subQueryValue.join(buffer[key]['options']['separator'] || ',');
} else if (typeof subQueryValue === 'object') {
//si c'est un object on le sérialise
subQueryValue = window['ATInternet']['Utils']['jsonSerialize'](subQueryValue);
}else if (typeof subQueryValue === 'undefined') {
subQueryValue = 'undefined';
} else {
subQueryValue = subQueryValue.toString();
}
if (buffer[key]['options']['encode']) {
subQueryValue = encodeURIComponent(subQueryValue);
}
var subQuery = _makeSubQuery(key, subQueryValue, window['ATInternet']['Utils']['arrayIndexOf'](multiHitable, key) > -1);
totalSize += subQuery.paramSize + subQuery.strSize;
if (!buffer[key]['options']['last']) {
subQueries[key] = subQuery;
if (subQueries[key].paramSize + subQueries[key].strSize > maxsize && !subQueries[key].truncate) {
//le paramètre est trop long et non troncable
sizeError = true;
keySizeError = key;
break;
}
} else {
//la paramètre avait l'option 'last', on le garde pour le placer à la fin du dictionnaire
if (subQueryValue.length > maxsize - 10) {
subQueryValue = subQueryValue.substr(0, maxsize - 10);
}
lastKey = key;
lastSubQuery = subQuery;
}
}
}
if (lastKey) {
subQueries[lastKey] = lastSubQuery;
}
return [subQueries, totalSize, sizeError, keySizeError];
};
/**
* Build at least one queryString (except for "?s=xxxx" parameter) using {@link ATInternet.Tracker.BufferManager parameters in the BufferManager}.
* <br />Indeed if the total size of the params involves a too long hit it's truncated in several hits (multihits).
* @memberof BuildManager#
* @function
* @param customParams {object} Custom parameters forced for queryString
* @param filters {Array} List of buffer filters
* @param callback {function} Callback which will use the queryString
* @returns {string|Array} String if the total size was short enough, other wise it will be an array.
* <br />Warning: it will be an array with one element if one of the parameter was too long and couldn't be truncated.
* @private
*/
var _buildParams = function (customParams, filters, callback) {
var createQueriesString = function (buffer) {
if (buffer === {}) {
return "";
}
var queries = [];
var resPreQuery = _preQuery(buffer, MAXSIZE);
var queryStrParams = resPreQuery[0];
var errorSize = resPreQuery[2];
if (errorSize) {
//Il y a une erreur sur le param resPreQuery[3]
//on génère donc un tableau à un seul élément pour signifier une erreur
//ce tableau contiendra un hit contenant le paramètre qui a causé l'erreur tronqué et
//le parametre 'mherr'
var paramError = resPreQuery[3];
var queryError = queryStrParams[paramError];
queryError['str'] = queryError['str'].substr(0, MAXSIZE - 50);
queryError['strSize'] = MAXSIZE - 50;
var newPreQueries = {};
newPreQueries['mherr'] = _makeSubQuery('mherr', '1', false);
newPreQueries[paramError] = queryError;
queries.push(_makeOneHit(newPreQueries, MAXSIZE)[0]);
} else {
var resHit = _makeOneHit(queryStrParams, MAXSIZE);
if (resHit[1] === null) {
//on n'est pas dans le cas d'un multihit donc on retourne une chaîne
queries = resHit[0];
} else {
//on est dans le cas d'un multihit
queries.push(resHit[0]);
while (resHit[1] !== null) {
resHit = _makeOneHit(queryStrParams, MAXSIZE);
queries.push(resHit[0]);
}
}
}
return queries;
};
var QueriesString = '';
if (!parent['buffer']['presentInFilters'](filters, 'hitType')) {
filters = parent['buffer']['addInFilters'](filters, 'hitType', ['page']);
}
filters = parent['buffer']['addInFilters'](filters, 'hitType', ['all']);
var params, k;
if(customParams) {
//recupérer les filtres et y ajouter celui des permanents
filters = parent['buffer']['addInFilters'](filters, 'permanent', true);
params = parent['buffer']['get'](filters, true);
for (k in customParams) {
if (customParams.hasOwnProperty(k)) {
params[k] = {
'value': customParams[k],
'options': {}
};
}
}
QueriesString = createQueriesString(params);
} else {
params = parent['buffer']['get'](filters, true);
QueriesString = createQueriesString(params);
//on sort du buffer tous les paramètres qui ne sont pas permanents
for (k in params) {
if (params.hasOwnProperty(k)) {
if (!params[k]['options']['permanent']) {
parent['buffer']['del'](k);
}
}
}
}
callback && callback(QueriesString);
};
/**
* Build the base URL with level 1 and call the callback given with it
* @memberof BuildManager#
* @function
* @param callback {function} Callback which will use the base URL
* @private
*/
var _buildConfig = function (callback) {
var configSecure = parent.getConfig('secure');
var protocolSecure = (document.location.protocol === _secureProtocol);
var isSecure = configSecure || protocolSecure;
var log = isSecure ? parent.getConfig('logSSL') : parent.getConfig('log');
var baseURL = parent.getConfig('baseURL');
var domain = parent.getConfig('domain');
var pixelPath = parent.getConfig('pixelPath');
var site = parent.getConfig('site');
if ((baseURL || (log && domain && pixelPath)) && site) {
domain = '.' + domain;
site = '?s=' + site;
var protocol = configSecure ? 'https://' : '//';
callback && callback(null, (baseURL ? baseURL : protocol + log + domain + pixelPath) + site);
}
else {
callback && callback({message: 'Config error'});
}
};
/**
* Build a list of hits (base URL + queryString)
* @memberof BuildManager#
* @function
* @param customParams {object} Custom parameters forced
* @param filters {Array} List of buffer filters
* @param callback {function} callback which will use the hit list
* @private
*/
var _build = function (customParams, filters, callback) {
_buildConfig(function(err,baseHit){
if(!err) {
_buildParams(customParams, filters, function(queriesString){
var hits = [];
var uuid = ATInternet.Utils.uuid();
var guid = uuid.num(13);
if (!(queriesString instanceof Array)) {
hits.push(baseHit + queriesString);
} else if (queriesString.length === 1) {
hits.push(baseHit + '&mh=1-2-' + guid + queriesString[0]);
} else {
for (var i = 1; i <= queriesString.length; i++) {
hits.push(baseHit + '&mh=' + i + '-' + queriesString.length + '-' + guid + queriesString[i-1]);
}
}
callback && callback(null, hits);
});
} else { //error
callback && callback(err);
}
});
};
/**
* Send at least one hit, more if it's too long (multihits).
* @name send
* @memberof BuildManager#
* @function
* @param customParams {object} Object which contains some hit parameters that you would like to send specifically (they are given priority over the current buffer)
* @param filters {Array} List of buffer filters
* @public
*/
self['send'] = function (customParams, filters) {
_build(customParams, filters, function (err, hits) {
if (!err) {
for (var i = 0; i < hits.length; i++) {
self.sendUrl(hits[i]);
}
}
else {
parent.emit('Tracker:Hit:Build:Error', {lvl:'ERROR', msg: err.message, details:{hits:hits}});
}
});
};
/**
* Send single hit from complete url.
* <br />An event will be sent {@link ATInternet.Tracker.TriggersManager} :
* <br />- "Tracker:Hit:Sent:Ok" with hit data if succeed
* <br />- "Tracker:Hit:Sent:Error" with error data otherwise
* @name sendUrl
* @memberof BuildManager#
* @function
* @param hit {string} Url to send
* @public
*/
self['sendUrl'] = function (hit) {
var makeTriggerCallback = function (trackerTrigger, hit) {
return (function () {
return function (evt) {
parent.emit(trackerTrigger, {lvl:(trackerTrigger.indexOf('Error')===-1)?'INFO':'ERROR', details:{hit:hit,event:evt}});
};
})();
};
var img = new Image();
img.onload = makeTriggerCallback('Tracker:Hit:Sent:Ok', hit);
img.onerror = makeTriggerCallback('Tracker:Hit:Sent:Error', hit);
img.src = hit;
};
// For unit tests on private elements !!!
/* @if test */
self['buildParams'] = _buildParams;
self['buildConfig'] = _buildConfig;
self['build'] = _build;
self['makeSubQuery'] = _makeSubQuery;
self['preQuery'] = _preQuery;
self['multiHitable'] = multiHitable;
self['makeOneHit'] = _makeOneHit;
self['secureProtocol'] = _secureProtocol;
/* @endif */
};