fiftyoneDegreesManager = function() { 'use-strict'; var json = {{&_jsonObject}}; var parameters = {{&_parameters}}; var sessionId = "{{&_sessionId}}"; var sessionKey = "{{_objName}}_" + sessionId; this.sessionId = sessionId; var sequence = {{&_sequence}}; // Log any errors returned in the JSON object. if(json.error !== undefined){ console.log(json.error); } // Log any warnings returned in the JSON object. if (json.warnings !== undefined) { console.log(json.warnings); } // Set to true when the JSON object is complete. var completed = false; // Set to true when the `catchError` is called. let failed = false; // changeFuncs is an array of functions. When onChange is called and passed // a function, the function is registered and is called when processing is // complete. var changeFuncs = []; // Counter is used to count how many pieces of callbacks are expected. Every // time the completedCallback method is called, the counter is decremented // by 1. var callbackCounter = 0; // Array of JavaScript properties that have started evaluation. var jsPropertiesStarted = []; // startsWith polyfill. var startsWith = function(source, searchValue) { return source.lastIndexOf(searchValue, 0) === 0; } // endsWith polyfill. var endsWith = function(source, searchValue) { return source.substring(source.length - searchValue.length, source.length) === searchValue; } var clearCache = function() { if (sessionStorage) { for (i = 0; i < sessionStorage.length; i++) { key = sessionStorage.key(i); if (startsWith(key, sessionKey)) { sessionStorage.removeItem(key); } } } } var loadParameters = function() { if (sessionStorage) { var parametersString = sessionStorage.getItem(sessionKey + "_parameters"); if (parametersString) { parameters = JSON.parse(parametersString); } } return parameters; } var saveParameters = function(sourceParams) { if (sourceParams) { parameters = sourceParams } if (sessionStorage) { var parametersString = JSON.stringify(parameters); sessionStorage.setItem(sessionKey + "_parameters", parametersString); } } // Get stored values with the '51D_' prefix that have been added to the request // and return the data as key value pairs. This method is needed to extract // stored values for inclusion in the GET or POST request for situations // where CORS will prevent them from being sent to third parties. var getFodSavedValues = function() { let fodValues = {}; {{#_enableCookies}} { let keyValuePairs = document.cookie.split(/; */); for(let nextPair of keyValuePairs) { let firstEqualsLocation = nextPair.indexOf("="); let name = nextPair.substring(0, firstEqualsLocation); if(startsWith(name, "51D_")){ let value = nextPair.substring(firstEqualsLocation+1); fodValues[name] = value; } } }; {{/_enableCookies}} {{^_enableCookies}} { // Collect values from session storage let session51DataPrefix = sessionKey + "_data_"; for(let i = 0, n = window.sessionStorage.length; i < n; ++i) { let nextKey = window.sessionStorage.key(i); if(startsWith(nextKey, session51DataPrefix)){ let value = window.sessionStorage[nextKey]; fodValues[nextKey.substring(session51DataPrefix.length)] = value; } } }; {{/_enableCookies}} return fodValues; }; // Extract key value pairs from the '51D_' prefixed values and concatenates // them to form a query string for the subsequent json refresh. var getParametersFromStorage = function() { var fodValues = getFodSavedValues(); var keyValuePairs = []; for (var key in fodValues) { if (fodValues.hasOwnProperty(key)) { // Url encode the value. // This is done to ensure that invalid characters (e.g. = chars at the end of // base 64 encoded strings) reach the server intact. // The server will automatically decode the value before passing it into the // Pipeline API. keyValuePairs.push(key + "=" + encodeURIComponent(fodValues[key])); } } return keyValuePairs; }; // Fetch a value safely from the json object. If a key somewhere down the // '.' separated hierarchy of keys is not present then 'undefined' is // returned rather than letting an exception occur. var getFromJson = function(key, allowObjects, allowBooleans) { var result = undefined; if (typeof allowObjects === "undefined") { allowObjects = false; } if (typeof allowBooleans === "undefined") { allowBooleans = false; } if (typeof(key) === "string") { var functions = json; var segments = key.split('.'); var i = 0; while (functions !== undefined && i < segments.length) { functions = functions[segments[i++]]; } if (typeof functions === "string") { result = functions; } else if (allowBooleans && typeof functions === "boolean") { result = functions; } else if (allowObjects && typeof functions === "object" && functions !== null) { result = functions; } } return result; } // Executed at the end of the processJSproperties method or for each piece // of JavaScript which has 51D code injected. When there are 0 pieces of // JavaScript left to process then reload the JSON object. var completedCallback = function(resolve, reject){ callbackCounter--; if (callbackCounter === 0) { {{#_updateEnabled}} processRequest(resolve, reject); {{/_updateEnabled}} } else if (callbackCounter < 0){ reject('Too many callbacks.'); } } // Executes any JavaScript contained in the JSON data. Session storage is // used to check the process state of the JavaScript property, if the name // of the property exists as a key then it has been processed. If all the // processed JavaScript properties have been flagged as processed already // then session storage is checked again for a JSON payload. If it exists // then this is loaded into the managers internal data store. If not or if // JavaScript properties have been processed then the call-back is // processed with any new evidence produced by the JavaScript properties. // If JavaScript properties are processed then a key containing the name of // the JavaScript property is added to session storage. The complete flag is // set to true when there is no further JavaScript to be processed. var processJsProperties = function(resolve, reject, jsProperties, ignoreDelayFlag) { var executeCallback = true; var started = 0; var cached = 0; var toProcess = 0; // If there is no cached response and there are JavaScript code snippets // then process them and perform any call-backs required. if (jsProperties !== undefined && jsProperties.length > 0) { {{^_enableCookies}} let valueSetPrefix = new RegExp('document\\.cookie\\s*=\\s*(("([A-Za-z0-9_"\\s\\+]+)\\s*=\\s*"\\s*\\+\\s*([^\\s};]+))|(`([A-Za-z0-9_]+)\\s*=\\s*\\$\\{([^}]+)\\}`))', 'g'); let session51DataPrefix = sessionKey + "_data_"; let sessionSetPatch = 'window.sessionStorage["' + session51DataPrefix + '$3$6"]=$4$7'; {{/_enableCookies}} // Execute each of the JavaScript property code snippets using the // index of the value to access the value to avoid problems with // JavaScript returning erroneous values. for (var index = 0; index < jsProperties.length; index++) { var name = jsProperties[index]; if (jsPropertiesStarted.indexOf(name) !== -1) { continue; } var body = getFromJson(name); // If there is a body then this property should be processed. if (body) { toProcess++; } var isCached = sessionStorage && sessionStorage.getItem(sessionKey + "_property_" + name); // If the property has already been processed then skip it. if (isCached) { cached++; continue; } // Create new function bound to this instance and execute it. // This is needed to ensure the scope of the function is // associated with this instance if any members are altered or // added. Avoids global scoped variables. var delay = getFromJson(name + 'delayexecution', false, true); if ( (ignoreDelayFlag || delay === undefined || delay === false) && typeof body === "string" && body.length ) { var func = undefined; var searchString = '// 51D replace this comment with callback function.'; completed = false; jsPropertiesStarted.push(name); started++; {{^_enableCookies}} body = body.replaceAll(valueSetPrefix, sessionSetPatch); {{/_enableCookies}} if (body.indexOf(searchString) !== -1){ callbackCounter++; body = body.replace(/\/\/ 51D replace this comment with callback function./g, 'callbackFunc(resolveFunc, rejectFunc);'); func = new Function('callbackFunc', 'resolveFunc', 'rejectFunc', "try {\n" + body + "\n" + "} catch (err) {\n" + "console.log(err);" + "}" ); func(completedCallback, resolve, reject); executeCallback = false; } else { func = new Function( "try {\n" + body + "\n" + "} catch (err) {\n" + "console.log(err);" + "}" ); func(); } if (sessionStorage) { sessionStorage.setItem(sessionKey + "_property_" + name, true) } // If the property is `javascripthardwareprofile` then check if the // profile has been set. If not then remove current property from // the list of properties that have started to prevent the // 2nd request from being made. if (name === "device.javascripthardwareprofile") { var hrw = getFodSavedValues(); if (hrw && !hrw["51D_ProfileIds"]) { // find and remove name from jsPropertiesStarted var propIndex = jsPropertiesStarted.indexOf(name); if (propIndex > -1) { jsPropertiesStarted.splice(index, 1); } started--; toProcess--; } } } } } if ((cached === toProcess || started === 0) && sessionStorage) { var cachedResponse = sessionStorage.getItem(sessionKey); if (cachedResponse) { loadJSON(resolve, reject, cachedResponse); executeCallback = false; } } if (started === 0) { executeCallback = false; failed = false; completed = true; } if (executeCallback) { callbackCounter = 1; completedCallback(resolve, reject); } }; {{#_updateEnabled}} {{^_supportsFetch}} // Standard method to create a CORS HTTP request ready to send data. var createCORSRequest = function(method, url) { var xhr; try { xhr = new XMLHttpRequest(); } catch(err){ xhr = null; } if (xhr !== null && "withCredentials" in xhr) { // Check if the XMLHttpRequest object has a "withCredentials" // property. // "withCredentials" only exists on XMLHTTPRequest2 objects. xhr.open(method, url, true); } else if (typeof XDomainRequest != "undefined") { // Otherwise, check if XDomainRequest. // XDomainRequest only exists in IE, and is IE's way of making CORS // requests. xhr = new XDomainRequest(); xhr.open(method, url); } else { // Otherwise, CORS is not supported by the browser. xhr = null; } return xhr; }; {{/_supportsFetch}} {{/_updateEnabled}} // Check if the JSON object still has any JavaScript snippets to run. var hasJSFunctions = function() { for (var i = i; i < json.javascriptProperties; i++) { var body = getFromJson(json.javascriptProperties[i]); if (body !== undefined && body.length > 0) { return true; } } return false; } // Process the JavaScript properties. var process = function(resolve, reject){ processJsProperties(resolve, reject, json.javascriptProperties, false); } var fireChangeFuncs = function(json) { for (var i = 0; i < changeFuncs.length; i++) { if (typeof changeFuncs[i] === 'function' && changeFuncs[i].length === 1) { changeFuncs[i](json); } } } {{#_updateEnabled}} // Process the response as json and call the resolve method. var loadJSON = function(resolve, reject, responseText) { try { json = JSON.parse(responseText); } catch(err) { clearCache(); reject(new Error("Invalid JSON - the endpoint is likely setup incorrectly", { cause: err })); return; } if (hasJSFunctions()) { // json updated so fire 'on change' functions // before executing any new JS properties that // have come back. fireChangeFuncs(json); process(resolve, reject); } else { failed = false; completed = true; // json updated so fire 'on change' functions // This must happen after completed = true in order // for 'complete' functions to fire. fireChangeFuncs(json); resolve(json); } } // Sends a POST request to the call-back URL to retrieve and updated // JSON payload from the cloud service. A POST request is used so that // parameters can be passed in the request body, this is to get around // the limitations on the length of query strings. As POST requests are not // cached by browsers, the result of the POST request is stored in session // storage on a successful response. This can then be checked before making // repeat requests to the call-back URL. // Any saved value parameters that have been set by the executed JavaScript // properties are added to the list of parameters, this list is then // serialized as Form Data and sent in the POST body to the call-back URL, // refreshing the JSON data. The new JSON is then loaded if the request is // returned with a success status code. If there was a problem then the // session storage items are invalidated and reject is called. var processRequest = function(resolve, reject){ loadParameters(); // Get additional parameters in case they are not sent // by the browser. var savedValueParams = getParametersFromStorage(); for(var savedValueIndex in savedValueParams) { var parts = savedValueParams[savedValueIndex].split('='); parameters[parts[0]] = parts[1]; } saveParameters(); var params = []; for (var param in parameters) { if (parameters.hasOwnProperty(param)) { params.push(param+"="+parameters[param]) } } // Add the session and sequence to the request if (sessionId) { params.push("session-id=" + sessionId); } if (sequence) { params.push("sequence=" + sequence); } var postBody = ""; if (params.length > 0) { postBody = params.join('&').replace(/%20/g, '+'); } {{#_supportsFetch}} fetch('{{{_url}}}', { method: 'POST', mode: 'cors', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: postBody }) .then(response => { return response.text(); }) .then(responseText => { {{/_supportsFetch}} {{^_supportsFetch}} // Request callback URL with additional parameters. var xhr = createCORSRequest('POST', '{{{_url}}}'); // If there is no support for HTTP requests then call reject and throw // a no CORS support error. if (!xhr) { reject(new Error('CORS not supported')); return; } // Add the HTTP header for POST form data. xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.setRequestHeader('Accept', 'application/json'); xhr.onload = function () { // Get the response body from the request. var responseText = xhr.responseText; {{/_supportsFetch}} // Cache the response text. if (sessionStorage) { sessionStorage.setItem(sessionKey, responseText); } // Load the JSON object from the response text loadJSON(resolve, reject, responseText); // Increment the sequence on a successful request sequence++; {{#_supportsFetch}} }) .catch(error => { // Invalidate the cache on error. clearCache(); reject(error); }); {{/_supportsFetch}} {{^_supportsFetch}} }; xhr.onerror = function () { // An error occurred with the request. Invalidate the cache and // return the details in the call to reject method. clearCache(); reject(Error(xhr.statusText)); }; // Send the GET request. xhr.send(postBody); {{/_supportsFetch}} } {{/_updateEnabled}} // Function logs errors, used to 'reject' a promise or for error callbacks. var catchError = function(value) { failed = true; function errorToDesc(subError) { let msg = subError.message; if (!msg) { msg = String(subError); } let cause = subError.cause; if (cause) { msg += '\n--- caused by ---\n'; msg += errorToDesc(cause); } return msg; } let errorDesc = errorToDesc(value); console.log(errorDesc); if (json.errors === null || json.errors === undefined) { json.errors = [errorDesc]; } else if (Array.isArray(json.errors)) { json.errors.push(errorDesc); } fireChangeFuncs(json); } // Populate this instance of the FOD object with getters to access the // properties. If the value is null then get the noValueMessage from the // JSON object corresponding to the property. var update = function(data){ var self = this; Object.getOwnPropertyNames(data).forEach(function(key) { self[key] = {}; for(var i in data[key]){ var obj = self[key]; (function(i) { Object.defineProperty(obj, i, { get: function (){ if(data[key][i] === null && (i !== "javascriptProperties")){ return data[key][i + "nullreason"]; } else { return data[key][i]; } } }) })(i); } }); } {{#_hasDelayedProperties}} // Get the JS property(s) that, when evaluated, will populate // evidence that can be used to determine the value of the // supplied property. // The supplied name can either be a complete property name or a top level // aspect name. // Where the aspect name is given, ALL evidence properties under that // key will be returned. // Example property names are 'location.country' or 'devices.profiles.hardwarename' // Example aspect names are 'location' or 'devices' var getEvidenceProperties = function (name) { var evidenceProperties = getFromJson(name + 'evidenceproperties'); if(typeof evidenceProperties === "undefined") { var item = getFromJson(name, true); evidenceProperties = getEvidencePropertiesFromObject(item); } return evidenceProperties; } // Get all values in any 'evidenceproperty' fields on this object // or sub-objects. var getEvidencePropertiesFromObject = function (dataObject) { evidenceProperties = []; for (var prop in dataObject) { if (dataObject.hasOwnProperty(prop)) { var value = dataObject[prop]; // Property name ends with 'evidenceproperties' so is // what we're looking for. // Add the values to the array if we don't already have it. if (value !== null && Array.isArray(value) && endsWith(prop, 'evidenceproperties')) { value.forEach(function(item, index) { if(evidenceProperties.indexOf(item) === -1) { evidenceProperties.push(item); } }); } // Item is an object so recursively call this method // and add any resulting evidence properties to the list. else if(typeof value === 'object' && value !== null) { getEvidencePropertiesFromObject(value).forEach(function(item, index) { if(evidenceProperties.indexOf(item) === -1) { evidenceProperties.push(item); } }); } } } return evidenceProperties; } {{/_hasDelayedProperties}} {{#_supportsPromises}} this.promise = new Promise(function(resolve, reject) { process(resolve,reject); }); {{/_supportsPromises}} this.onChange = function(resolve) { changeFuncs.push(resolve); } this.complete = function(resolve, properties) { {{#_hasDelayedProperties}} // If properties is set then check if we need to kick off // processing of anything. if(typeof properties !== "undefined") { // If properties is a string then split on comma to produce // an array of one or more key names. if(typeof properties === "string") { properties = properties.split(','); } if(Array.isArray(properties)) { properties.forEach(function(key, i) { // We pass an empty function rather than 'resolve' because we // don't want to call resolve when a single evidence function // evaluates but after all of them have completed. // This is handled by the 'if(complete)' code below. processJsProperties(function(json) {}, catchError, getEvidenceProperties(key), true); }); } } {{/_hasDelayedProperties}} if(completed || failed){ resolve(json); }else{ this.onChange(function(data) { if(completed || failed){ resolve(data); } }) } }; // Update this instance with the initial JSON payload. update.call(this, json); {{#_supportsPromises}} var parent = this; this.promise.then(function(value) { // JSON has been updated so replace the current instance. update.call(parent, value); failed = false; completed = true; }).catch(catchError); {{/_supportsPromises}} {{^_supportsPromises}} process(function(json) {}, catchError); {{/_supportsPromises}} } var {{_objName}} = new fiftyoneDegreesManager();