diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e69de29..0000000 diff --git a/composer.json b/composer.json index 6844627..268a827 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,8 @@ "ext-ctype": "*", "ext-json": "*", "mustache/mustache": "^2.13", - "tedivm/jshrink": "~1.0" + "tedivm/jshrink": "~1.0", + "51degrees/fiftyone.pipeline.javascript-templates": "1.*" }, "require-dev": { "kint-php/kint": "^3.3", @@ -41,5 +42,12 @@ "psr-4": { "fiftyone\\pipeline\\core\\tests\\": "tests/" } - } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/51Degrees/javascript-templates", + "no-api": true + } + ] } diff --git a/src/JavaScriptResource.mustache b/src/JavaScriptResource.mustache deleted file mode 100644 index 170b022..0000000 --- a/src/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/src/JavascriptBuilderElement.php b/src/JavascriptBuilderElement.php index 2704fbf..0bf6452 100644 --- a/src/JavascriptBuilderElement.php +++ b/src/JavascriptBuilderElement.php @@ -122,17 +122,16 @@ public function processInternal($flowData) $vars["_host"] = $host; $vars["_protocol"] = $protocol; + $params = $this->getEvidenceKeyFilter()->filterEvidence($flowData->evidence->getAll()); + if ($vars["_host"] && $vars["_protocol"] && $vars["_endpoint"]) { $vars["_url"] = $vars["_protocol"] . "://" . $vars["_host"] . $vars["_endpoint"]; // Add query parameters to the URL - - $queryParams = $this->getEvidenceKeyFilter()->filterEvidence($flowData->evidence->getAll()); - $query = []; - foreach ($queryParams as $param => $paramValue) { + foreach ($params as $param => $paramValue) { $paramKey = explode(".", $param)[1]; $query[$paramKey] = $paramValue; @@ -167,8 +166,18 @@ public function processInternal($flowData) // Check if any delayedproperties exist in the json $vars["_hasDelayedProperties"] = strpos($vars["_jsonObject"], "delayexecution") !== false; - - $output = $m->render(file_get_contents(__DIR__ . "/JavaScriptResource.mustache"), $vars); + $vars["_sessionId"] = $flowData->evidence->get("query.session-id"); + $vars["_sequence"] = $flowData->evidence->get("query.sequence"); + + $jsParams = []; + foreach ($params as $param => $paramValue) { + $paramKey = explode(".", $param)[1]; + $jsParams[$paramKey] = $paramValue; + } + + $vars["_parameters"] = json_encode($jsParams); + + $output = $m->render(file_get_contents($this->getTemplatePath()), $vars); if($this->minify) { // Minify the output @@ -181,4 +190,19 @@ public function processInternal($flowData) return; } + + private function getTemplatePath() + { + $templatePath = '51degrees/fiftyone.pipeline.javascript-templates/JavaScriptResource.mustache'; + $prefixes = ['/../../../', '/../vendor/']; + + foreach ($prefixes as $prefix) { + $path = __DIR__ . $prefix . $templatePath; + if (file_exists($path)) { + return $path; + } + } + + throw new \Exception('Could not find JavaScriptResource template'); + } }