diff --git a/fiftyone.pipeline.core/JavaScriptResource.mustache b/fiftyone.pipeline.core/JavaScriptResource.mustache deleted file mode 100644 index 170b022..0000000 --- a/fiftyone.pipeline.core/JavaScriptResource.mustache +++ /dev/null @@ -1,442 +0,0 @@ -fiftyoneDegreesManager = function() { - 'use-strict'; - var json = {{&_jsonObject}}; - - // Log any errors returned in the JSON object. - if(json.error !== undefined){ - console.log(json.error); - } - - // Set to true when the JSON object is complete. - var completed = 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; - } - - // Get cookies 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 - // cookie values for inclusion in the GET or POST request for situations - // where CORS will prevent cookies being sent to third parties. - var getFodCookies = function(){ - var keyValuePairs = document.cookie.split(/; */); - var fodCookies = []; - for(var i = 0; i < keyValuePairs.length; i++) { - var name = keyValuePairs[i].substring(0, keyValuePairs[i].indexOf('=')); - if(startsWith(name, "51D_")){ - var value = keyValuePairs[i].substring(keyValuePairs[i].indexOf('=')+1); - fodCookies[name] = value; - } - } - return fodCookies; - }; - - // Extract key value pairs from the '51D_' prefixed cookies and concatenates - // them to form a query string for the subsequent json refresh. - var getParametersFromCookies = function(){ - var fodCookies = getFodCookies(); - var keyValuePairs = []; - for (var key in fodCookies) { - if (fodCookies.hasOwnProperty(key)) { - keyValuePairs.push(key+"="+fodCookies[key]); - } - } - return keyValuePairs; - }; - - // Delete a cookie. - function deleteCookie(name) { - document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT;'; - } - - // 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. Sets the processedJs - // flag to true when there is no further Javascript to be processed. - var processJsProperties = function(resolve, reject, jsProperties, ignoreDelayFlag) { - var executeCallback = true; - var started = 0; - - if (jsProperties !== undefined && - jsProperties.length > 0) { - - // 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.includes(name) === false) { - // 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 body = getFromJson(name); - var delay = getFromJson(name + 'delayexecution', false, true); - - if ((ignoreDelayFlag || (delay === undefined || delay === false)) && - body !== undefined) { - var func = undefined; - var searchString = '// 51D replace this comment with callback function.'; - completed = false; - jsPropertiesStarted.push(name); - started++; - - 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(started === 0) { - executeCallback = false; - completed = true; - } - if(executeCallback) { - callbackCounter = 1; - completedCallback(resolve, reject); - } - }; - -{{#_updateEnabled}} - // 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; - }; -{{/_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}} - // Sends the cookie parameters that have been set by the executed Javascript - // in the POST body of a new request to refresh the json data. Resolve is - // called with the new json if the request is processed as expected. Reject - // is called if there was a problem. The parameters are send as a POST - // request so that encoding and data length issues are minimised. - var processRequest = function(resolve, reject){ - // Request URL with a license key and User-Agent if provided. - 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.onload = function () { - - // Get the response body from the request. - var responseText = xhr.responseText; - - // Process the response as json and call the resolve method. - json = JSON.parse(responseText); - - 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 { - 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); -{{^_enableCookies}} - var fodCookies = getFodCookies(); - for (var key in fodCookies) { - if (fodCookies.hasOwnProperty(key)) { - deleteCookie(key); - } - } -{{/_enableCookies}} - } - - }; - - xhr.onerror = function () { - // An error occurred with the request. Return the details in the - // call to reject method. - reject(Error(xhr.statusText)); - }; - - // Get additional parameters from cookies in case they are not sent - // by the browser. - var params = getParametersFromCookies(); - - // Send the cookie parameters as the POST body. - xhr.send(params.join('&').replace(/%20/g, '+')); - } -{{/_updateEnabled}} - - // Function logs errors, used to 'reject' a promise or for error callbacks. - var catchError = function(value) { - console.log(value.message || value); - } - - // 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) && prop.endsWith('evidenceproperties')) { - value.forEach(function(item, index) { - if(evidenceProperties.includes(item) === false) { - 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.includes(item) === false) { - 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){ - resolve(json); - }else{ - this.onChange(function(data) { - if(completed){ - 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); - completed = true; - }).catch(catchError); -{{/_supportsPromises}} -{{^_supportsPromises}} - process(function(json) {}, catchError); -{{/_supportsPromises}} -} - -var {{_objName}} = new fiftyoneDegreesManager(); \ No newline at end of file diff --git a/fiftyone.pipeline.core/constants.js b/fiftyone.pipeline.core/constants.js new file mode 100644 index 0000000..549dc61 --- /dev/null +++ b/fiftyone.pipeline.core/constants.js @@ -0,0 +1,26 @@ +/* ********************************************************************* + * This Original Work is copyright of 51 Degrees Mobile Experts Limited. + * Copyright 2023 51 Degrees Mobile Experts Limited, Davidson House, + * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. + * + * This Original Work is licensed under the European Union Public Licence + * (EUPL) v.1.2 and is subject to its terms as set out below. + * + * If a copy of the EUPL was not distributed with this file, You can obtain + * one at https://opensource.org/licenses/EUPL-1.2. + * + * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be + * amended by the European Commission) shall be deemed incompatible for + * the purposes of the Work and the provisions of the compatibility + * clause in Article 5 of the EUPL shall not apply. + * + * If using the Work as, or as part of, a network application, by + * including the attribution notice(s) required under Article 5 of the EUPL + * in the end user terms of the application under an appropriate heading, + * such notice(s) shall fulfill the requirements of that article. + * ********************************************************************* */ + +module.exports = { + evidenceEnableCookies: 'query.fod-js-enable-cookies', + evidenceObjectName: 'query.fod-js-object-name' +}; diff --git a/fiftyone.pipeline.core/index.js b/fiftyone.pipeline.core/index.js index 8d6bf59..792b5d9 100644 --- a/fiftyone.pipeline.core/index.js +++ b/fiftyone.pipeline.core/index.js @@ -25,6 +25,7 @@ module.exports = { ElementData: require('./elementData'), ElementDataDictionary: require('./elementDataDictionary'), ErrorMessages: require('./errorMessages'), + Constants: require('./constants.js'), Evidence: require('./evidence'), EvidenceKeyFilter: require('./evidenceKeyFilter'), FlowData: require('./flowData'), diff --git a/fiftyone.pipeline.core/javascript-templates b/fiftyone.pipeline.core/javascript-templates index 1e2c80d..a0642e3 160000 --- a/fiftyone.pipeline.core/javascript-templates +++ b/fiftyone.pipeline.core/javascript-templates @@ -1 +1 @@ -Subproject commit 1e2c80d2b7ab50e0f6a6ea765204fcf61cd9207b +Subproject commit a0642e361691cf9368253497a850eb060a4eb010 diff --git a/fiftyone.pipeline.core/javascriptbuilder.js b/fiftyone.pipeline.core/javascriptbuilder.js index 7775c35..4ce5c6a 100644 --- a/fiftyone.pipeline.core/javascriptbuilder.js +++ b/fiftyone.pipeline.core/javascriptbuilder.js @@ -33,6 +33,7 @@ const template = fs.readFileSync( const FlowElement = require('./flowElement.js'); const EvidenceKeyFilter = require('./evidenceKeyFilter.js'); const ElementDataDictionary = require('./elementDataDictionary.js'); +const Constants = require('./constants.js'); const uglifyJS = require('uglify-js'); /** @@ -73,7 +74,10 @@ class JavaScriptBuilderElement extends FlowElement { * callback url. This can be overriden with header.host evidence. * @param {string} options.endPoint The endpoint of the client side * callback url - * @param {boolean} options.enableCookies whether cookies should be enabled + * @param {boolean} options.enableCookies Whether the client JavaScript + * stored results of client side processing in cookies. This can also + * be set per request, using the "query.fod-js-enable-cookies" evidence key. + * For more details on personal data policy, see http://51degrees.com/terms/client-services-privacy-policy/ * @param {boolean} options.minify Whether to minify the JavaScript */ constructor ({ @@ -199,6 +203,17 @@ class JavaScriptBuilderElement extends FlowElement { settings._sessionId = flowData.evidence.get('query.session-id'); settings._sequence = flowData.evidence.get('query.sequence'); + // Try and get the requested enable cookies from evidence. + const enableCookies = flowData.evidence.get(Constants.evidenceEnableCookies); + if (enableCookies !== undefined) { + settings._enableCookies = (enableCookies?.toLowerCase?.() === 'true'); + } + // Try and get the requested object name from evidence. + const objName = flowData.evidence.get(Constants.evidenceObjectName); + if (objName !== undefined) { + settings._objName = (objName); + } + let output = mustache.render(template, settings); if (settings._minify) { diff --git a/fiftyone.pipeline.core/tests/enableCookies.test.js b/fiftyone.pipeline.core/tests/enableCookies.test.js new file mode 100644 index 0000000..c83662a --- /dev/null +++ b/fiftyone.pipeline.core/tests/enableCookies.test.js @@ -0,0 +1,77 @@ +/* ********************************************************************* + * This Original Work is copyright of 51 Degrees Mobile Experts Limited. + * Copyright 2023 51 Degrees Mobile Experts Limited, Davidson House, + * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. + * + * This Original Work is licensed under the European Union Public Licence + * (EUPL) v.1.2 and is subject to its terms as set out below. + * + * If a copy of the EUPL was not distributed with this file, You can obtain + * one at https://opensource.org/licenses/EUPL-1.2. + * + * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be + * amended by the European Commission) shall be deemed incompatible for + * the purposes of the Work and the provisions of the compatibility + * clause in Article 5 of the EUPL shall not apply. + * + * If using the Work as, or as part of, a network application, by + * including the attribution notice(s) required under Article 5 of the EUPL + * in the end user terms of the application under an appropriate heading, + * such notice(s) shall fulfill the requirements of that article. + * ********************************************************************* */ + +const core = require('fiftyone.pipeline.core'); +const each = require('jest-each').default; + +const cookieElement = new core.FlowElement({ + dataKey: 'cookie', + properties: { + javascript: { + type: 'javascript' + } + }, + + processInternal: function (flowData) { + const contents = { javascript: 'document.cookie = "some cookie value"' }; + + const data = new core.ElementDataDictionary({ + flowElement: this, + contents + }); + + flowData.setElementData(data); + } + +}); + +const sequenceElement = new core.SequenceElement(); +const jsonElement = new core.JsonBundler(); +each([ + [false, false, false], + [true, false, false], + [false, true, true], + [true, true, true] +]) + .test('JavaScript cookies', (enableInConfig, enableInEvidence, expectCookie, done) => { + const jsElement = new core.JavascriptBuilder({ enableCookies: enableInConfig }); + + const pipeline = new core.PipelineBuilder() + .add(cookieElement) + .add(sequenceElement) + .add(jsonElement) + .add(jsElement) + .build(); + + const flowData = pipeline.createFlowData(); + flowData.evidence.add(core.Constants.evidenceEnableCookies, enableInEvidence.toString()); + flowData.process().then(function () { + const js = flowData.javascriptbuilder.javascript; + const matches = [...js.matchAll(/document\.cookie/g)]; + if (expectCookie) { + expect(matches.length).toBe(2); + } else { + expect(matches.length).toBe(1); + } + done(); + }); + });