fiftyone.pipeline.core 4.4.104 → 4.4.105

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,6 +20,9 @@ fiftyoneDegreesManager = function() {
20
20
  // Set to true when the JSON object is complete.
21
21
  var completed = false;
22
22
 
23
+ // Set to true when the `catchError` is called.
24
+ let failed = false;
25
+
23
26
  // changeFuncs is an array of functions. When onChange is called and passed
24
27
  // a function, the function is registered and is called when processing is
25
28
  // complete.
@@ -79,13 +82,13 @@ fiftyoneDegreesManager = function() {
79
82
  // and return the data as key value pairs. This method is needed to extract
80
83
  // stored values for inclusion in the GET or POST request for situations
81
84
  // where CORS will prevent them from being sent to third parties.
82
- var getFodSavedValues = function(){
85
+ var getFodSavedValues = function() {
83
86
  let fodValues = {};
84
87
  {{#_enableCookies}}
85
88
  {
86
89
  let keyValuePairs = document.cookie.split(/; */);
87
90
  for(let nextPair of keyValuePairs) {
88
- let firstEqualsLocation = nextPair.indexOf('=');
91
+ let firstEqualsLocation = nextPair.indexOf("=");
89
92
  let name = nextPair.substring(0, firstEqualsLocation);
90
93
  if(startsWith(name, "51D_")){
91
94
  let value = nextPair.substring(firstEqualsLocation+1);
@@ -112,17 +115,17 @@ fiftyoneDegreesManager = function() {
112
115
 
113
116
  // Extract key value pairs from the '51D_' prefixed values and concatenates
114
117
  // them to form a query string for the subsequent json refresh.
115
- var getParametersFromStorage = function(){
118
+ var getParametersFromStorage = function() {
116
119
  var fodValues = getFodSavedValues();
117
120
  var keyValuePairs = [];
118
121
  for (var key in fodValues) {
119
122
  if (fodValues.hasOwnProperty(key)) {
120
123
  // Url encode the value.
121
- // This is done to ensure that invalid characters (e.g. = chars at the end of
124
+ // This is done to ensure that invalid characters (e.g. = chars at the end of
122
125
  // base 64 encoded strings) reach the server intact.
123
- // The server will automatically decode the value before passing it into the
126
+ // The server will automatically decode the value before passing it into the
124
127
  // Pipeline API.
125
- keyValuePairs.push(key+"="+encodeURIComponent(fodValues[key]));
128
+ keyValuePairs.push(key + "=" + encodeURIComponent(fodValues[key]));
126
129
  }
127
130
  }
128
131
  return keyValuePairs;
@@ -133,21 +136,21 @@ fiftyoneDegreesManager = function() {
133
136
  // returned rather than letting an exception occur.
134
137
  var getFromJson = function(key, allowObjects, allowBooleans) {
135
138
  var result = undefined;
136
- if(typeof allowObjects === 'undefined') { allowObjects = false; }
137
- if(typeof allowBooleans === 'undefined') { allowBooleans = false; }
139
+ if (typeof allowObjects === "undefined") { allowObjects = false; }
140
+ if (typeof allowBooleans === "undefined") { allowBooleans = false; }
138
141
 
139
- if (typeof(key) === 'string') {
142
+ if (typeof(key) === "string") {
140
143
  var functions = json;
141
144
  var segments = key.split('.');
142
145
  var i = 0;
143
146
  while (functions !== undefined && i < segments.length) {
144
147
  functions = functions[segments[i++]];
145
148
  }
146
- if (typeof(functions) === "string") {
149
+ if (typeof functions === "string") {
147
150
  result = functions;
148
- } else if (allowBooleans && typeof(functions) === "boolean") {
151
+ } else if (allowBooleans && typeof functions === "boolean") {
149
152
  result = functions;
150
- } else if (allowObjects && typeof functions === 'object' && functions !== null) {
153
+ } else if (allowObjects && typeof functions === "object" && functions !== null) {
151
154
  result = functions;
152
155
  }
153
156
  }
@@ -183,7 +186,6 @@ fiftyoneDegreesManager = function() {
183
186
  var executeCallback = true;
184
187
  var started = 0;
185
188
  var cached = 0;
186
- var cachedResponse = undefined;
187
189
  var toProcess = 0;
188
190
 
189
191
  // If there is no cached response and there are JavaScript code snippets
@@ -195,87 +197,113 @@ fiftyoneDegreesManager = function() {
195
197
  let session51DataPrefix = sessionKey + "_data_";
196
198
  let sessionSetPatch = 'window.sessionStorage["' + session51DataPrefix + '$3$6"]=$4$7';
197
199
  {{/_enableCookies}}
198
-
200
+
199
201
  // Execute each of the JavaScript property code snippets using the
200
202
  // index of the value to access the value to avoid problems with
201
203
  // JavaScript returning erroneous values.
202
204
  for (var index = 0; index < jsProperties.length; index++) {
203
205
  var name = jsProperties[index];
204
- if (jsPropertiesStarted.indexOf(name) === -1) {
205
- var body = getFromJson(name);
206
+ if (jsPropertiesStarted.indexOf(name) !== -1) {
207
+ continue;
208
+ }
209
+ var body = getFromJson(name);
210
+
211
+ // If there is a body then this property should be processed.
212
+ if (body) {
213
+ toProcess++;
214
+ }
215
+
216
+ var isCached = sessionStorage && sessionStorage.getItem(sessionKey + "_property_" + name);
206
217
 
207
- // If there is a body then this property should be processed.
208
- if (body) {
209
- toProcess++;
218
+ // If the property has already been processed then skip it.
219
+ if (isCached) {
220
+ cached++;
221
+ continue;
222
+ }
223
+
224
+ // Create new function bound to this instance and execute it.
225
+ // This is needed to ensure the scope of the function is
226
+ // associated with this instance if any members are altered or
227
+ // added. Avoids global scoped variables.
228
+
229
+ var delay = getFromJson(name + 'delayexecution', false, true);
230
+
231
+ if (
232
+ (ignoreDelayFlag || delay === undefined || delay === false) &&
233
+ typeof body === "string" &&
234
+ body.length
235
+ ) {
236
+ var func = undefined;
237
+ var searchString = '// 51D replace this comment with callback function.';
238
+ completed = false;
239
+ jsPropertiesStarted.push(name);
240
+ started++;
241
+
242
+ {{^_enableCookies}}
243
+ body = body.replaceAll(valueSetPrefix, sessionSetPatch);
244
+ {{/_enableCookies}}
245
+
246
+ if (body.indexOf(searchString) !== -1){
247
+ callbackCounter++;
248
+ body = body.replace(/\/\/ 51D replace this comment with callback function./g, 'callbackFunc(resolveFunc, rejectFunc);');
249
+ func = new Function('callbackFunc', 'resolveFunc', 'rejectFunc',
250
+ "try {\n" +
251
+ body + "\n" +
252
+ "} catch (err) {\n" +
253
+ "console.log(err);" +
254
+ "}"
255
+ );
256
+ func(completedCallback, resolve, reject);
257
+ executeCallback = false;
258
+ } else {
259
+ func = new Function(
260
+ "try {\n" +
261
+ body + "\n" +
262
+ "} catch (err) {\n" +
263
+ "console.log(err);" +
264
+ "}"
265
+ );
266
+ func();
210
267
  }
211
- var isCached = sessionStorage && sessionStorage.getItem(sessionKey + "_property_" + name);
212
-
213
- if (!isCached) {
214
- // Create new function bound to this instance and execute it.
215
- // This is needed to ensure the scope of the function is
216
- // associated with this instance if any members are altered or
217
- // added. Avoids global scoped variables.
218
-
219
- var delay = getFromJson(name + 'delayexecution', false, true);
220
-
221
- if ((ignoreDelayFlag || (delay === undefined || delay === false)) &&
222
- body !== undefined) {
223
- var func = undefined;
224
- var searchString = '// 51D replace this comment with callback function.';
225
- completed = false;
226
- jsPropertiesStarted.push(name);
227
- started++;
228
-
229
- {{^_enableCookies}}
230
- body = body.replaceAll(valueSetPrefix, sessionSetPatch);
231
- {{/_enableCookies}}
232
-
233
- if (body.indexOf(searchString) !== -1){
234
- callbackCounter++;
235
- body = body.replace(/\/\/ 51D replace this comment with callback function./g, 'callbackFunc(resolveFunc, rejectFunc);');
236
- func = new Function('callbackFunc', 'resolveFunc', 'rejectFunc',
237
- "try {\n" +
238
- body + "\n" +
239
- "} catch (err) {\n" +
240
- "console.log(err);" +
241
- "}"
242
- );
243
- func(completedCallback, resolve, reject);
244
- executeCallback = false;
245
- } else {
246
- func = new Function(
247
- "try {\n" +
248
- body + "\n" +
249
- "} catch (err) {\n" +
250
- "console.log(err);" +
251
- "}"
252
- );
253
- func();
254
- }
255
- if (sessionStorage) {
256
- sessionStorage.setItem(sessionKey + "_property_" + name, true)
268
+
269
+ if (sessionStorage) {
270
+ sessionStorage.setItem(sessionKey + "_property_" + name, true)
271
+ }
272
+
273
+ // If the property is `javascripthardwareprofile` then check if the
274
+ // profile has been set. If not then remove current property from
275
+ // the list of properties that have started to prevent the
276
+ // 2nd request from being made.
277
+ if (name === "device.javascripthardwareprofile") {
278
+ var hrw = getFodSavedValues();
279
+ if (hrw && !hrw["51D_ProfileIds"]) {
280
+ // find and remove name from jsPropertiesStarted
281
+ var propIndex = jsPropertiesStarted.indexOf(name);
282
+ if (propIndex > -1) {
283
+ jsPropertiesStarted.splice(index, 1);
257
284
  }
285
+ started--;
286
+ toProcess--;
258
287
  }
259
- } else {
260
- cached++;
261
288
  }
262
289
  }
263
290
  }
264
291
  }
265
- if(cached === toProcess || started === 0) {
266
- if (sessionStorage) {
267
- var cachedResponse = sessionStorage.getItem(sessionKey);
268
- if (cachedResponse) {
269
- loadJSON(resolve, reject, cachedResponse);
270
- executeCallback = false;
271
- }
292
+
293
+ if ((cached === toProcess || started === 0) && sessionStorage) {
294
+ var cachedResponse = sessionStorage.getItem(sessionKey);
295
+ if (cachedResponse) {
296
+ loadJSON(resolve, reject, cachedResponse);
297
+ executeCallback = false;
272
298
  }
273
299
  }
274
300
 
275
301
  if (started === 0) {
276
302
  executeCallback = false;
303
+ failed = false;
277
304
  completed = true;
278
305
  }
306
+
279
307
  if (executeCallback) {
280
308
  callbackCounter = 1;
281
309
  completedCallback(resolve, reject);
@@ -343,7 +371,13 @@ fiftyoneDegreesManager = function() {
343
371
  {{#_updateEnabled}}
344
372
  // Process the response as json and call the resolve method.
345
373
  var loadJSON = function(resolve, reject, responseText) {
346
- json = JSON.parse(responseText);
374
+ try {
375
+ json = JSON.parse(responseText);
376
+ } catch(err) {
377
+ clearCache();
378
+ reject(new Error("Invalid JSON - the endpoint is likely setup incorrectly", { cause: err }));
379
+ return;
380
+ }
347
381
 
348
382
  if (hasJSFunctions()) {
349
383
  // json updated so fire 'on change' functions
@@ -352,6 +386,7 @@ fiftyoneDegreesManager = function() {
352
386
  fireChangeFuncs(json);
353
387
  process(resolve, reject);
354
388
  } else {
389
+ failed = false;
355
390
  completed = true;
356
391
  // json updated so fire 'on change' functions
357
392
  // This must happen after completed = true in order
@@ -476,7 +511,28 @@ fiftyoneDegreesManager = function() {
476
511
 
477
512
  // Function logs errors, used to 'reject' a promise or for error callbacks.
478
513
  var catchError = function(value) {
479
- console.log(value.message || value);
514
+ failed = true;
515
+ function errorToDesc(subError) {
516
+ let msg = subError.message;
517
+ if (!msg) {
518
+ msg = String(subError);
519
+ }
520
+ let cause = subError.cause;
521
+ if (cause) {
522
+ msg += '\n--- caused by ---\n';
523
+ msg += errorToDesc(cause);
524
+ }
525
+ return msg;
526
+ }
527
+ let errorDesc = errorToDesc(value);
528
+ console.log(errorDesc);
529
+
530
+ if (json.errors === null || json.errors === undefined) {
531
+ json.errors = [errorDesc];
532
+ } else if (Array.isArray(json.errors)) {
533
+ json.errors.push(errorDesc);
534
+ }
535
+ fireChangeFuncs(json);
480
536
  }
481
537
 
482
538
  // Populate this instance of the FOD object with getters to access the
@@ -588,11 +644,11 @@ fiftyoneDegreesManager = function() {
588
644
  }
589
645
 
590
646
  {{/_hasDelayedProperties}}
591
- if(completed){
647
+ if(completed || failed){
592
648
  resolve(json);
593
649
  }else{
594
650
  this.onChange(function(data) {
595
- if(completed){
651
+ if(completed || failed){
596
652
  resolve(data);
597
653
  }
598
654
  })
@@ -606,6 +662,7 @@ fiftyoneDegreesManager = function() {
606
662
  this.promise.then(function(value) {
607
663
  // JSON has been updated so replace the current instance.
608
664
  update.call(parent, value);
665
+ failed = false;
609
666
  completed = true;
610
667
  }).catch(catchError);
611
668
  {{/_supportsPromises}}
@@ -6,6 +6,53 @@ Store shared (mustache) templates to be used by the implementation of `JavaScrip
6
6
 
7
7
  The [language-independent specification](https://github.com/51Degrees/specifications) describes how JavaScriptBuilderElement is used [here](https://github.com/51Degrees/specifications/blob/36ff732360acb49221dc81237281264dac4eb897/pipeline-specification/pipeline-elements/javascript-builder.md). The mechanics is: javascript file is requested from the server (on-premise web integration) and is created from this mustache template by the JavaScriptBuilderElement. It then collects more evidence, sends it to the server and upon response calls a callback function providing the client with more precise device data.
8
8
 
9
+ ## Cookies -> Session storage Transformation
10
+
11
+ The processJsProperties function in javascript template has a section that uses regex to search the injected JavaScript for any commands that set cookie values to the browser's document object. It then transforms this command so that it sets session storage values instead. This format should be accounted for when writing JavaScript properties.
12
+
13
+ ---
14
+
15
+ ### Cookie Transformation regular expression rules
16
+
17
+ #### Valid Format for Cookie Statements:
18
+ - **Cookie Assignment**: The expression should start with `document.cookie = `
19
+ - **Spaces**: Spaces around the first `=` sign are optional
20
+ - **Cookie Name**: The name of the cookie should only contain alphanumeric characters, underscores, and must not have spaces
21
+ - **Assignment with Double Quotes**: The cookie value assignment can use double quotes, and the value should be set programmatically by concatenating a string with a variable or expression
22
+ - **Assignment with Backticks**: The cookie value assignment can use backticks for template literals, and the value can be set programmatically using expressions inside `${}`
23
+ - **No Direct Value Assignment**: Directly setting a value within the string is not allowed; values must be set programmatically
24
+
25
+ #### Regular Expression:
26
+ ```javascript
27
+ /document\.cookie\s*=\s*(("([A-Za-z0-9_"\s\+]+)\s*=\s*"\s*\+\s*([^\s};]+))|(`([A-Za-z0-9_]+)\s*=\s*\$\{([^}]+)\}`))/g
28
+ ```
29
+
30
+ #### Valid Examples:
31
+ ```javascript
32
+ document.cookie="51D_PropertyName="+"True"; // No spaces around the equals and/or plus sign
33
+ document.cookie = "51D_PropertyName=" + "True"; // Spaces around the equals sign
34
+ document.cookie = "51D_PropertyName=" + screen.height; // Assigning a value using a variable
35
+ document.cookie="51D_PropertyName="+screen.height; // No spaces, variable assignment
36
+ document.cookie=`51D_PropertyName=${btoa(JSON.stringify(value))}` // Using a template literal with an expression
37
+ document.cookie="51D_PropertyName="+profileIds.join("|") // Assigning a value using a joined string of variables
38
+ document.cookie = `51D_PropertyName=${"True"}`; // Using backticks for programmatic value assignment
39
+ ```
40
+
41
+ #### Invalid Examples:
42
+ ```javascript
43
+ document.cookie = "51D_PropertyName=True"; // Direct assignment within the string is not allowed
44
+ document.cookie = "51D_PropertyName=" + profileIds.join(" ") // Spaces within the expression are not allowed
45
+ document.cookie = " 51D_PropertyName = " + "True"; // Spaces inside the cookie name are not allowed
46
+ document.cookie = ` 51D_PropertyName =${"True"}`; // Spaces inside the template literal are not allowed
47
+ document.cookie = `51D_PropertyName=START${window.middle}END`; // Concatenating strings directly within template literals is not allowed
48
+ ```
49
+ ---
50
+
51
+ ## CSP Considerations
52
+ [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) is an added layer of security to mitigate cross-site and other types of attacks. CSP limits which 3rd party resources are loaded and what these resources are allowed to do. 51Degrees JavaScript produced from the template is usually such a 3rd party resource when hosted on [51Degrees cloud](https://cloud.51degrees.com/api-docs/index.html). If CSP header specifies [script-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src) it has to list 51Degrees cloud origin as a source and also add 'unsafe-eval' as a source.
53
+
54
+ `'unsafe-eval'` source is needed because the template loads and executes dynamic javascript code snippets relying on JavaScript Function API which is in the eval() family. The snippets are part of the data file and are frequently updated to support latest changes in the browsers. Snippet execution may cause multiple server calls to load more dynamic code (in theory, in practice it usually comes down to a single server call) - thus this code can not be statically included in the template and has to be loaded dynamically as part of the JSON response of the server.
55
+
9
56
  ## Shipping / Deployment
10
57
 
11
58
  This repo is not a stand-alone package, but is shipped as part of and used by each of the following repositories / packages:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fiftyone.pipeline.core",
3
- "version": "4.4.104",
3
+ "version": "4.4.105",
4
4
  "description": "Core library for the 51Degrees Pipeline API",
5
5
  "keywords": [
6
6
  "51degrees",