Skip to content

Commit 85b401e

Browse files
FilipNestSteve51D
authored andcommitted
FEAT: Add the ability to delay the execution of client-side evidence properties.
This is used to control the execution of JavaScript that would cause UI elements to be show to the user. For example, the 'allow website to access your location' popup. Changes to json bundler, javascript builder (minor new setting change) and tests. Related work items: #3826
1 parent d6654a9 commit 85b401e

File tree

4 files changed

+388
-79
lines changed

4 files changed

+388
-79
lines changed

JavaScriptResource.mustache

Lines changed: 135 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ fiftyoneDegreesManager = function() {
2020
// by 1.
2121
var callbackCounter = 0;
2222

23+
// Array of JavaScript properties that have started evaluation.
24+
var jsPropertiesStarted = [];
25+
2326
// startsWith polyfill.
2427
var startsWith = function(source, searchValue){
2528
return source.lastIndexOf(searchValue, 0) === 0;
@@ -63,8 +66,11 @@ fiftyoneDegreesManager = function() {
6366
// Fetch a value safely from the json object. If a key somewhere down the
6467
// '.' separated hierarchy of keys is not present then 'undefined' is
6568
// returned rather than letting an exception occur.
66-
var getFromJson = function(key) {
69+
var getFromJson = function(key, allowObjects, allowBooleans) {
6770
var result = undefined;
71+
if(typeof allowObjects === 'undefined') { allowObjects = false; }
72+
if(typeof allowBooleans === 'undefined') { allowBooleans = false; }
73+
6874
if (typeof(key) === 'string') {
6975
var functions = json;
7076
var segments = key.split('.');
@@ -74,6 +80,10 @@ fiftyoneDegreesManager = function() {
7480
}
7581
if (typeof(functions) === "string") {
7682
result = functions;
83+
} else if (allowBooleans && typeof(functions) === "boolean") {
84+
result = functions;
85+
} else if (allowObjects && typeof functions === 'object' && functions !== null) {
86+
result = functions;
7787
}
7888
}
7989
return result;
@@ -95,56 +105,66 @@ fiftyoneDegreesManager = function() {
95105

96106
// Executes any Javascript contained in the json data. Sets the processedJs
97107
// flag to true when there is no further Javascript to be processed.
98-
var processJSproperties = function(resolve, reject) {
108+
var processJsProperties = function(resolve, reject, jsProperties, ignoreDelayFlag) {
99109
var executeCallback = true;
110+
var started = 0;
100111
101-
if (json.javascriptProperties !== undefined &&
102-
json.javascriptProperties.length > 0) {
112+
if (jsProperties !== undefined &&
113+
jsProperties.length > 0) {
103114
104115
// Execute each of the Javascript property code snippets using the
105116
// index of the value to access the value to avoid problems with
106117
// JavaScript returning erroneous values.
107118
for(var index = 0;
108-
index < json.javascriptProperties.length;
119+
index < jsProperties.length;
109120
index++) {
110121
111-
var name = json.javascriptProperties[index];
112-
113-
// Create new function bound to this instance and execute it.
114-
// This is needed to ensure the scope of the function is
115-
// associated with this instance if any members are altered or
116-
// added. Avoids global scoped variables.
117-
var body = getFromJson(name);
118-
119-
if (body !== undefined) {
120-
var func = undefined;
121-
var searchString = '// 51D replace this comment with callback function.';
122-
123-
if(body.indexOf(searchString) !== -1){
124-
callbackCounter++;
125-
body = body.replace(/\/\/ 51D replace this comment with callback function./g, 'callbackFunc(resolveFunc, rejectFunc);');
126-
func = new Function('callbackFunc', 'resolveFunc', 'rejectFunc',
127-
"try {\n" +
128-
body + "\n" +
129-
"} catch (err) {\n" +
130-
"console.log(err);" +
131-
"}"
132-
);
133-
func(completedCallback, resolve, reject);
134-
executeCallback = false;
135-
} else {
136-
func = new Function(
137-
"try {\n" +
138-
body + "\n" +
139-
"} catch (err) {\n" +
140-
"console.log(err);" +
141-
"}"
142-
);
143-
func();
122+
var name = jsProperties[index];
123+
124+
if(jsPropertiesStarted.includes(name) === false) {
125+
// Create new function bound to this instance and execute it.
126+
// This is needed to ensure the scope of the function is
127+
// associated with this instance if any members are altered or
128+
// added. Avoids global scoped variables.
129+
var body = getFromJson(name);
130+
var delay = getFromJson(name + 'delayexecution', false, true);
131+
132+
if ((ignoreDelayFlag || (delay === undefined || delay === false)) &&
133+
body !== undefined) {
134+
var func = undefined;
135+
var searchString = '// 51D replace this comment with callback function.';
136+
completed = false;
137+
jsPropertiesStarted.push(name);
138+
started++;
139+
140+
if(body.indexOf(searchString) !== -1){
141+
callbackCounter++;
142+
body = body.replace(/\/\/ 51D replace this comment with callback function./g, 'callbackFunc(resolveFunc, rejectFunc);');
143+
func = new Function('callbackFunc', 'resolveFunc', 'rejectFunc',
144+
"try {\n" +
145+
body + "\n" +
146+
"} catch (err) {\n" +
147+
"console.log(err);" +
148+
"}"
149+
);
150+
func(completedCallback, resolve, reject);
151+
executeCallback = false;
152+
} else {
153+
func = new Function(
154+
"try {\n" +
155+
body + "\n" +
156+
"} catch (err) {\n" +
157+
"console.log(err);" +
158+
"}"
159+
);
160+
func();
161+
}
144162
}
145163
}
146164
}
147-
} else {
165+
}
166+
167+
if(started === 0) {
148168
executeCallback = false;
149169
completed = true;
150170
}
@@ -198,7 +218,7 @@ fiftyoneDegreesManager = function() {
198218
199219
// Process the JavaScript properties.
200220
var process = function(resolve, reject){
201-
processJSproperties(resolve, reject);
221+
processJsProperties(resolve, reject, json.javascriptProperties, false);
202222
}
203223
204224
var fireChangeFuncs = function(json) {
@@ -308,6 +328,59 @@ fiftyoneDegreesManager = function() {
308328
});
309329
}
310330

331+
{{#_hasDelayedProperties}}
332+
// Get the JS property(s) that, when evaluated, will populate
333+
// evidence that can be used to determine the value of the
334+
// supplied property.
335+
// The supplied name can either be a complete property name or a top level
336+
// aspect name.
337+
// Where the aspect name is given, ALL evidence properties under that
338+
// key will be returned.
339+
// Example property names are 'location.country' or 'devices.profiles.hardwarename'
340+
// Example aspect names are 'location' or 'devices'
341+
var getEvidenceProperties = function (name) {
342+
var evidenceProperties = getFromJson(name + 'evidenceproperties');
343+
if(typeof evidenceProperties === "undefined") {
344+
var item = getFromJson(name, true);
345+
evidenceProperties = getEvidencePropertiesFromObject(item);
346+
}
347+
return evidenceProperties;
348+
}
349+
350+
// Get all values in any 'evidenceproperty' fields on this object
351+
// or sub-objects.
352+
var getEvidencePropertiesFromObject = function (dataObject) {
353+
evidenceProperties = [];
354+
355+
for (var prop in dataObject) {
356+
if (dataObject.hasOwnProperty(prop)) {
357+
var value = dataObject[prop];
358+
// Property name ends with 'evidenceproperties' so is
359+
// what we're looking for.
360+
// Add the values to the array if we don't already have it.
361+
if (value !== null && Array.isArray(value) && prop.endsWith('evidenceproperties')) {
362+
value.forEach(function(item, index) {
363+
if(evidenceProperties.includes(item) === false) {
364+
evidenceProperties.push(item);
365+
}
366+
});
367+
}
368+
// Item is an object so recursively call this method
369+
// and add any resulting evidence properties to the list.
370+
else if(typeof value === 'object' && value !== null) {
371+
getEvidencePropertiesFromObject(value).forEach(function(item, index) {
372+
if(evidenceProperties.includes(item) === false) {
373+
evidenceProperties.push(item);
374+
}
375+
});
376+
}
377+
}
378+
}
379+
380+
return evidenceProperties;
381+
}
382+
{{/_hasDelayedProperties}}
383+
311384
{{#_supportsPromises}}
312385
this.promise = new Promise(function(resolve, reject) {
313386
process(resolve,reject);
@@ -318,7 +391,28 @@ fiftyoneDegreesManager = function() {
318391
changeFuncs.push(resolve);
319392
}
320393

321-
this.complete = function(resolve) {
394+
this.complete = function(resolve, properties) {
395+
{{#_hasDelayedProperties}}
396+
// If properties is set then check if we need to kick off
397+
// processing of anything.
398+
if(typeof properties !== "undefined") {
399+
// If properties is a string then split on comma to produce
400+
// an array of one or more key names.
401+
if(typeof properties === "string") {
402+
properties = properties.split(',');
403+
}
404+
if(Array.isArray(properties)) {
405+
properties.forEach(function(key, i) {
406+
// We pass an empty function rather than 'resolve' because we
407+
// don't want to call resolve when a single evidence function
408+
// evaluates but after all of them have completed.
409+
// This is handled by the 'if(complete)' code below.
410+
processJsProperties(function(json) {}, catchError, getEvidenceProperties(key), true);
411+
});
412+
}
413+
}
414+
415+
{{/_hasDelayedProperties}}
322416
if(completed){
323417
resolve(json);
324418
}else{
@@ -337,7 +431,6 @@ fiftyoneDegreesManager = function() {
337431
this.promise.then(function(value) {
338432
// JSON has been updated so replace the current instance.
339433
update.call(parent, value);
340-
resolve(parent);
341434
completed = true;
342435
}).catch(catchError);
343436
{{/_supportsPromises}}

JavascriptBuilder.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,11 @@ public function processInternal($flowData)
162162
} else {
163163
$vars["_supportsPromises"] = false;
164164
}
165-
165+
166+
// Check if any delayedproperties exist in the json
167+
168+
$vars["_hasDelayedProperties"] = strpos($vars["_jsonObject"], "delayexecution") !== false;
169+
166170
$output = $m->render(file_get_contents(__DIR__ . "/JavaScriptResource.mustache"), $vars);
167171

168172
if($this->minify) {

0 commit comments

Comments
 (0)