-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathJavaScriptResource.mustache
More file actions
1594 lines (1480 loc) · 71.1 KB
/
Copy pathJavaScriptResource.mustache
File metadata and controls
1594 lines (1480 loc) · 71.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
fiftyoneDegreesManager = function() {
'use strict';
var json = {{&_jsonObject}};
// The query evidence of the script request, rendered by the builder and
// already url encoded by it. A factory rather than an object, so that
// every request body is built from a fresh copy and the rendered values
// are never changed in place.
var renderedParameters = function() { return {{&_parameters}}; };
var sessionId = "{{&_sessionId}}";
// The session id is deliberately not part of the key: it changes on
// every request, and session storage is already scoped to one tab.
var sessionKey = "{{_objName}}";
// Where the record of the last request's inputs is kept. It starts with
// the storage key and an underscore, so clearCache takes it away with
// everything else.
var inputsKey = sessionKey + "_inputs";
this.sessionId = sessionId;
// Held so that loadParsedJSON can update this instance whatever path it
// was reached by.
var instance = this;
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 = [];
// Properties whose snippets have run but whose evidence has not yet reached
// the server. They are flagged in session storage only once the response
// they feed is cached, so a flag can never be present without the payload
// it belongs to.
var jsPropertiesPending = [];
// The record of the inputs of the last request this script dispatched,
// or of the request that produced the payload in session storage. A
// refresh whose inputs match it has nothing to ask for.
var lastSent = null;
// Set when an answer arrives while a round is in progress. That round's
// own next request is built from the answer as it stands, so it carries
// the answer already. Only an answer that arrives after the round's last
// request needs a request of its own, and this flag is what asks for one.
var refreshPending = false;
// The number of iterations of one page view. The server stops listing
// snippets at the same number, so nothing is sent after it. This matches
// MAX_JAVASCRIPT_ITERATIONS in pipeline-dotnet's JSON builder constants
// and a test there compares the two.
var maxIterations = 10;
// Replaced inside the update section below, where a request can actually
// be made. A script rendered with updates disabled never makes one, so
// there is nothing for a refresh to do and page code is told so.
var refresh = function() {
console.log("51Degrees: updates are disabled for this script, refresh() does nothing.");
};
// Run at the end of every round, whether it completed or failed. A round
// is the whole exchange, from process() through every snippet callback to
// the response that lists nothing more, which is what completed or failed
// already expresses. The callback counter is not built for two rounds at
// once and in flight is the wrong unit, because the snippets run
// asynchronously before the first request goes.
var roundEnded = function() {
if (refreshPending) {
refreshPending = false;
refresh();
return;
}
{{#_updateEnabled}}
{{#_userPrompt}}
// A round can end with no request having been made at all, which is
// what happens where the server asked for no snippet, or asked only
// for one that saved nothing and so had its count backed out again,
// because processJsProperties then completes from the payload
// rendered into the page. Where the visitor had already answered
// before this script ran and the request for the script did not carry
// that answer, the answer has reached nobody and no 51Did can have
// been created, so the round is asked for here. refresh() decides
// whether anything is really needed and does nothing where the inputs
// match what was last sent, so the ordinary page, whose own request
// already carried the answer, stays at one request and cannot loop.
// Only a round that completed counts, because a round that failed had
// dispatched a request and that request carried the answer.
if (completed && answerNotSent()) {
refresh();
}
{{/_userPrompt}}
{{/_updateEnabled}}
};
// 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() {
// Guarded with typeof because the script is also evaluated where
// session storage is not declared at all, which is how the cloud's
// two builder unit tests run it.
if (typeof sessionStorage !== 'undefined' && sessionStorage) {
// Iterate backwards: removeItem shifts every later key down one
// index, so a forward loop skips whichever key moves into the slot
// just vacated.
try {
for (var i = sessionStorage.length - 1; i >= 0; i--) {
var key = sessionStorage.key(i);
// Match the exact key or the key plus a separator. Without
// the session id the prefix is short enough that a bare
// startsWith would also match an unrelated key on the same
// origin.
if (key === sessionKey || startsWith(key, sessionKey + "_")) {
sessionStorage.removeItem(key);
}
}
} catch (err) {
// Web storage is blocked, so there is nothing stored to clear.
}
}
}
// Read the cached payload, or null when there is none or it cannot be used.
// An entry that does not parse into an object is cleared here rather than
// ignored: this runs before the '_property_' flags are read, so a bad entry
// makes this page view execute the snippets again instead of completing
// with neither the cached values nor a request.
var getCachedJson = function() {
if (typeof sessionStorage === 'undefined' || !sessionStorage) {
return null;
}
var cachedResponse = null;
try {
cachedResponse = sessionStorage.getItem(sessionKey);
} catch (err) {
// Reading web storage throws where the visitor has blocked it, and
// a page view with no storage still has to run.
return null;
}
if (!cachedResponse) {
return null;
}
var cachedJson = null;
try {
cachedJson = JSON.parse(cachedResponse);
} catch (err) {
// Handled by the check below, which also covers a payload that
// parses to null or to a value that is not an object.
}
if (typeof cachedJson !== 'object' || cachedJson === null) {
clearCache();
return null;
}
return cachedJson;
}
// Suffix the server appends to a property name to carry the reason that
// property has no value.
var nullReasonSuffix = 'nullreason';
// Cached values take precedence over the payload rendered for this page
// view. The cached payload is the response to a request that carried the
// JavaScript evidence, whereas the payload rendered into this page view was
// answered from headers alone, so the cached value is the better-informed
// one. Leaving the rendered value in place would discard exactly what the
// cache exists to preserve: a property the server can answer from headers
// is not null, so its ambiguous answer would survive - on an iPhone
// 'hardwarename' is the whole candidate list rather than the resolved model.
var mergeCached = function(cachedJson) {
Object.getOwnPropertyNames(cachedJson).forEach(function(group) {
var cachedGroup = cachedJson[group];
// Only aspect objects carry cached values. The arrays at this level
// (javascriptProperties, errors, warnings) describe the request that
// produced them, so taking them from an earlier page view would
// resurrect a stale error or a snippet list the server no longer
// sends.
if (typeof cachedGroup !== 'object' ||
cachedGroup === null ||
Array.isArray(cachedGroup)) {
return;
}
var freshGroup = json[group];
if (freshGroup === undefined || freshGroup === null) {
json[group] = cachedGroup;
return;
}
// Anything the server has just supplied as a non-object wins, and
// is not something values can be merged into.
if (typeof freshGroup !== 'object' || Array.isArray(freshGroup)) {
return;
}
for (var name in cachedGroup) {
// Reasons are carried with the value they describe, below, so
// that a cached reason is never left next to a value it did not
// arrive with.
if (endsWith(name, nullReasonSuffix)) {
continue;
}
// A cached null says only that the earlier response could not
// answer either, so it is not worth losing the fresh value for.
if (cachedGroup[name] === null || cachedGroup[name] === undefined) {
continue;
}
freshGroup[name] = cachedGroup[name];
var reasonName = name + nullReasonSuffix;
if (cachedGroup[reasonName] === undefined) {
delete freshGroup[reasonName];
} else {
freshGroup[reasonName] = cachedGroup[reasonName];
}
}
});
return json;
}
// 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}}
{
// Collect values from session storage, which holds the values the
// snippets produced where cookies are off, and in both modes the
// empty result of a snippet that stored nothing. A name the
// cookies already carry is left alone, so a value a snippet
// stored beats the empty result written before it ran. Guarded
// because the request body is now built in the constructor as
// well as at dispatch, so this runs where there is no window at
// all and where the visitor has blocked storage.
try {
if (typeof window !== 'undefined' && window.sessionStorage) {
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 name =
nextKey.substring(session51DataPrefix.length);
// A name holding a quote was stored by an earlier
// version of this script, which took the text of
// a join for a fixed name. No snippet writes a
// result by that name, so it is not sent.
if(name.indexOf('"') !== -1){
continue;
}
if(!Object.prototype.hasOwnProperty.call(
fodValues, name)){
fodValues[name] = window.sessionStorage[nextKey];
}
}
}
}
} catch (err) {
// Storage is blocked, so there are no stored values.
}
};
return fodValues;
};
// Url encode one key or one value for the request body. This is done so
// that invalid characters, such as the = at the end of a base 64 encoded
// string, reach the server intact, and the server decodes the value
// before passing it into the Pipeline API. The rendered parameters arrive
// already encoded by the builder, so they go into the map as they stand
// and everything added below is encoded as it is added, which leaves
// every pair encoded exactly once.
var encodePart = function(value) {
return encodeURIComponent(value);
};
// Build the map of everything a request body carries, each key once,
// later sources replacing earlier ones. The order is the rendered
// parameters of this page view, then the stored '51D_' values the
// snippets produced, then the evidence object the page supplied, read at
// this moment. Building one map rather than pushing two lists is what
// stops a key given both in the script URL and in the evidence object
// being sent twice, which the server keeps as a repeated form key that
// every reader then sees as absent.
var buildBody = function() {
var body = renderedParameters();
// The values the snippets produced. They are the one thing that
// carries over from an earlier page view, and they come from their
// own storage rather than from a stored copy of the parameters.
var savedValues = getFodSavedValues();
for (var savedKey in savedValues) {
if (Object.prototype.hasOwnProperty.call(savedValues, savedKey)) {
body[encodePart(savedKey)] = encodePart(savedValues[savedKey]);
}
}
// Additional evidence provided by the page. Sent in the request body,
// never put in the URL or a cookie, and kept in session storage only
// inside the record of the request's inputs.
var pageEvidence = typeof window !== 'undefined' ?
window["{{_objName}}Evidence"] : undefined;
if (pageEvidence && typeof pageEvidence === "object") {
for (var evidenceKey in pageEvidence) {
if (Object.prototype.hasOwnProperty.call(pageEvidence, evidenceKey)) {
body[encodePart(evidenceKey)] =
encodePart(pageEvidence[evidenceKey]);
}
}
}
{{#_updateEnabled}}
{{#_userPrompt}}
// The block's answer goes into the body and never onto the evidence
// object. One value for one key, so the answer replaces whatever the
// script URL or the evidence object carried. The two Global Privacy
// Platform keys go with it, because the server no longer reads them
// and there is no point carrying them. A page with no platform at all
// sends whatever its URL and evidence object carried, as before.
if (answer) {
delete body["id.usage"];
delete body["tcstring"];
delete body["gpp"];
delete body["gppstring"];
var mark = answer.indexOf("=");
body[answer.substring(0, mark)] =
encodePart(answer.substring(mark + 1));
}
{{/_userPrompt}}
{{/_updateEnabled}}
return body;
};
// The map as the wire sees it, one 'key=value' string per entry.
var toPairs = function(body) {
var pairs = [];
for (var key in body) {
if (Object.prototype.hasOwnProperty.call(body, key)) {
pairs.push(key + "=" + body[key]);
}
}
return pairs;
};
// The keys the record never carries. Both change on every request, so a
// record holding either could never match the next page view's and the
// cached response could never be reused. Where updates are enabled the
// request appends them after the record is taken, which is enough where
// they reach the body from nowhere else, and they are taken out here as
// well because a builder is free to render whatever it likes into the
// script's parameters and one that renders these lands them in the body.
// This comment does not name the function that sends the request,
// because it sits outside the update section and a script rendered with
// updates off is checked for that name being absent. Taking
// them out in one place means the record says what its name says whatever
// the parameters carried.
var recordExcluded = { "session-id": 1, "sequence": 1 };
// The record of a request's inputs. The same pairs as the body, without
// the session id and the sequence, sorted so that the order the keys
// happened to be added in cannot make two identical requests look
// different, and joined the way the body joins them. It is kept as this
// plain string and is deliberately not hashed, because session storage on
// the publisher's origin is reachable only by the joint controllers,
// being the publisher and 51Degrees, and a hash would be read as a
// privacy measure when it is not one.
var toRecord = function(body) {
var pairs = [];
for (var key in body) {
if (Object.prototype.hasOwnProperty.call(body, key) &&
Object.prototype.hasOwnProperty.call(recordExcluded, key)
=== false) {
pairs.push(key + "=" + body[key]);
}
}
return pairs.sort().join('&').replace(/%20/g, '+');
};
// The record of what this page view would send if it dispatched now.
var inputs = function() {
return toRecord(buildBody());
};
{{#_updateEnabled}}
{{#_userPrompt}}
// The user prompt block. The script gathers the visitor's answer itself
// and the publisher writes no code. The 51Degrees Preference Management
// Platform is asked first and the Transparency and Consent Framework
// second, the first source with an answer wins and the rest are ignored,
// and the server applies the same order. A Global Privacy Platform string
// is never read, because the Model Terms for Marketing do not map it, so
// nothing here registers on the Global Privacy Platform's window object
// and neither gpp nor gppstring is ever sent.
//
// Every browser global below is reached through typeof and every call
// into a platform, every storage read and every JSON.parse sits in a try
// whose catch means no answer from that source. The script is evaluated
// with no window at all by two builder unit tests, a visitor can block
// web storage, a platform's stub can throw on any command, and nothing
// here may stop process().
// The answer as one body key and its value, such as "id.usage=standard",
// or undefined where no source has one.
var answer = undefined;
// The keys an answer can occupy in the body.
var answerKeys = ["id.usage", "tcstring"];
// The last value the platform's window event delivered.
var promptEventValue = undefined;
// The last framework string delivered with success true and a status the
// specification says carries a complete string.
var frameworkString = undefined;
// The three answers the Preference Management Platform can hold. The
// alternative button stores non-marketing, which is an answer under the
// Model Terms and not a refusal, so it is sent as a usage like the other
// two. There is no refusal in the platform.
var promptAnswers = ["non-marketing", "standard", "personalized"];
// True where a consent platform is on the page, whether or not it has
// answered yet. The page's own document is deliberately not searched for
// the platform's script tag, because a tag that follows this one has not
// been parsed when this runs, so the search would answer no in the one
// case it was meant to cover. Answering no where a platform is present
// costs one extra round, whilst answering yes where none is present would
// reuse a stored answer that no longer applies.
var platformPresent = function() {
if (typeof window === 'undefined') {
return false;
}
try {
if (typeof window.__tcfapi === 'function') {
return true;
}
if (typeof window.__51d_pmp !== 'undefined' &&
window.__51d_pmp !== null) {
return true;
}
} catch (err) {
// A stub that throws on a property read tells us nothing, so it
// counts as no platform.
}
return false;
};
// The Preference Management Platform's answer, or undefined.
var readPromptAnswer = function() {
if (typeof window === 'undefined') {
return undefined;
}
// Once the bundle has loaded it holds in memory whatever init() found,
// in local storage or in the shared store, which is how a returning
// visitor whose answer lives in the shared store has a synchronous
// answer on a page that loaded the platform first.
try {
if (typeof window.__51d_pmp !== 'undefined' &&
window.__51d_pmp !== null &&
typeof window.__51d_pmp.preference === 'function') {
var held = window.__51d_pmp.preference();
if (promptAnswers.indexOf(held) !== -1) {
return held;
}
}
} catch (err) {
// No answer from that source.
}
if (promptAnswers.indexOf(promptEventValue) !== -1) {
return promptEventValue;
}
// Covers a script that constructs before the bundle has loaded on a
// site where the visitor has already answered. Nothing new is written
// to storage by either side.
try {
if (typeof localStorage !== 'undefined' && localStorage) {
var stored = localStorage.getItem("__51d_pmp_pref");
if (stored) {
var parsed = JSON.parse(stored);
if (parsed && promptAnswers.indexOf(parsed.p) !== -1) {
return parsed.p;
}
}
}
} catch (err) {
// Storage is blocked or the entry does not parse, so there is no
// answer from that source.
}
return undefined;
};
// One reader, run at construction and on every wake up. The priority
// chain runs every time, so there is no answered flag, a later value from
// the source that answered replaces the answer, and a delivery from a
// lower ranked source changes nothing. That is what makes a visitor
// who chooses standard and then the alternative in the same page view, or
// whose answer arrives from the shared store after this script
// constructed, produce the fresh request the rule about reuse requires.
var readAnswer = function() {
var preference = readPromptAnswer();
if (preference) {
return "id.usage=" + preference;
}
if (typeof frameworkString === 'string' && frameworkString.length > 0) {
// A string is never mapped to a usage here. IabTcfElement does
// that on the server and two copies would drift.
return "tcstring=" + frameworkString;
}
return undefined;
};
// True where the block holds an answer that no request has carried. The
// request for this script itself counts, because the payload rendered
// into the page was built from that request's own query evidence, so a
// page whose script URL already named the answer has the identifier in
// its rendered payload and nothing new to send. Where a request has since
// gone, refresh() is the one that notices, by comparing the inputs with
// the record of what was last sent.
var answerNotSent = function() {
if (!answer) {
return false;
}
var mark = answer.indexOf("=");
return renderedParameters()[answer.substring(0, mark)] !==
encodePart(answer.substring(mark + 1));
};
// The wake up.
var wake = function() {
var a = readAnswer();
if (a !== answer) {
answer = a;
refresh();
}
};
// Read a stored record back into its map, so that one key can be taken
// from it. The record is written in the same encoding the body uses, so
// the pairs come back exactly as they went in.
var recordToMap = function(record) {
var map = {};
if (!record) {
return map;
}
var pairs = record.split('&');
for (var i = 0; i < pairs.length; i++) {
var mark = pairs[i].indexOf('=');
if (mark > 0) {
map[pairs[i].substring(0, mark)] =
pairs[i].substring(mark + 1);
}
}
return map;
};
// Registered once at construction and never removed.
var registerListeners = function() {
if (typeof window === 'undefined') {
return;
}
// The platform's bundle loads asynchronously and may arrive after
// this script in either tag order, so this is registered whether or
// not the platform has loaded. It is the only route by which an
// answer, the alternative answer and a correction from the shared
// store reach this script as a stated usage.
try {
if (typeof window.addEventListener === 'function') {
window.addEventListener('51d-pmp-preference', function(event) {
try {
var delivered = event && event.detail ?
event.detail.preference : undefined;
// Only the three answers are accepted. Anything else
// is ignored rather than stored, so a delivery this
// script does not recognise cannot throw away an
// answer the visitor has already given.
if (promptAnswers.indexOf(delivered) !== -1) {
promptEventValue = delivered;
}
} catch (err) {
// No answer from that source.
}
wake();
});
}
} catch (err) {
console.warn("51Degrees: registering the preference listener threw, so a preference announced by the page will not be seen.");
}
// The framework specification requires the callback to be invoked at
// once with the current data where the platform is loaded, and
// getTCData is deprecated in version 2.2 and not required, so neither
// ping nor getTCData is called. A page's stub queues the registration
// until the platform loads and calls back then, which is the normal
// route on a page with a third party platform.
try {
if (typeof window.__tcfapi === 'function') {
var calledBack = false;
window.__tcfapi('addEventListener', 2, function(data, success) {
calledBack = true;
try {
if (success !== true || !data) {
// A delivery of (null, false) clears it and is
// never read as an answer. On the Preference
// Management Platform it is only the framework's
// view of the alternative answer, a usage granting
// no purposes, which this block reads through the
// platform source, and on any other platform it is
// an error.
frameworkString = undefined;
} else if (data.eventStatus === 'tcloaded' ||
data.eventStatus === 'useractioncomplete') {
frameworkString =
typeof data.tcString === 'string' &&
data.tcString.length > 0 ?
data.tcString : undefined;
}
} catch (err) {
frameworkString = undefined;
}
wake();
});
// This only logs. It changes nothing about processing and it
// is not a wait.
if (typeof setTimeout === 'function') {
setTimeout(function() {
if (calledBack === false) {
console.warn("51Degrees: the call to __tcfapi('addEventListener') has not called back within ten seconds, so it is treated as no answer.");
}
}, 10000);
}
}
} catch (err) {
console.warn("51Degrees: the call to __tcfapi('addEventListener') threw, so it is treated as no answer.");
}
};
// Where no platform ever appears and no source has an answer there is no
// recovery on this page view, and this warning is the only signal, so the
// case it covers matters. It is deliberately not printed at construction,
// because a page's platform tag is ordinarily asynchronous and a platform
// that has not registered yet is not a platform that is not coming. An
// answer from any source, including one that arrives during the wait
// below, means a 51Did will be created, so saying otherwise would be
// wrong and nothing is printed. No value is ever printed by this block.
var sayNoPlatform = function() {
if (!answer && platformPresent() === false) {
console.warn("51Degrees: no preference platform was found on this page. A platform's stub must precede this script. No 51Did will be created until a platform answers.");
}
};
// The wait before that warning, matching the one a platform makes for
// this script's object so that neither side calls the other absent whilst
// it is still on its way. The wait ends at the page's load event, by
// which time every tag the page's markup carries has run, and after 5000
// milliseconds at the latest for a page whose load event is very late or
// never comes, whichever of the two comes first. Where the page has
// already loaded nothing is waited for, because a platform that is not
// there then is not coming from a tag. This only logs. It changes nothing
// about processing and no request waits for it.
var warnIfNoPlatform = function() {
// Nothing to wait for where there is no page to load, which is the
// server side evaluation that renders this script.
if (typeof window === 'undefined' || window === null ||
typeof window.addEventListener !== 'function' ||
typeof setTimeout !== 'function') {
sayNoPlatform();
return;
}
// An answer or a platform already in hand, so there is nothing this
// wait could change.
if (answer || platformPresent()) {
return;
}
try {
if (typeof document !== 'undefined' && document &&
document.readyState === 'complete') {
sayNoPlatform();
return;
}
} catch (err) {
// A document that throws on a read tells us nothing about the
// page's state, so the wait below covers it.
}
// Either end runs this, and only the first of them counts.
var ended = false;
var end = function() {
if (ended === true) {
return;
}
ended = true;
try {
if (typeof window.removeEventListener === 'function') {
window.removeEventListener('load', end, false);
}
} catch (err) {
// There is nothing left to remove.
}
sayNoPlatform();
};
try {
window.addEventListener('load', end, false);
} catch (err) {
// The page's load event cannot be heard, so the limit below is
// the only end to the wait.
}
setTimeout(end, 5000);
};
{{/_userPrompt}}
{{/_updateEnabled}}
// Compare this page view's inputs with the record of the request that
// produced the entry in session storage. Runs once per script run, in the
// constructor, before the first round. Not inside process(), which is
// re-entered once per round and would clear the round's own values part
// way through an exchange.
var applyInputsRecord = function() {
if (typeof sessionStorage === 'undefined' || !sessionStorage) {
return;
}
var stored = null;
try {
stored = sessionStorage.getItem(inputsKey);
} catch (err) {
// Storage is blocked, so there is no record to compare with.
return;
}
if (!stored) {
return;
}
var current = buildBody();
{{#_updateEnabled}}
{{#_userPrompt}}
// No answer yet means unknown, not different. Where a platform is on
// the page but has not delivered, the answer key in the stored record
// stands in for the missing one in this comparison, so the entry is
// served until the platform answers and an answer that differs then
// refreshes. Without this a page whose platform loads after this
// script would pay two full rounds on every page view for the life of
// the tab. A page with no platform at all keeps the literal reading.
if (!answer && platformPresent()) {
var storedMap = recordToMap(stored);
for (var i = 0; i < answerKeys.length; i++) {
if (Object.prototype.hasOwnProperty.call(
storedMap, answerKeys[i])) {
current[answerKeys[i]] = storedMap[answerKeys[i]];
}
}
}
{{/_userPrompt}}
{{/_updateEnabled}}
if (toRecord(current) === stored) {
// The same inputs, so the stored answer may be reused and a later
// refresh that changes nothing has nothing to ask for.
lastSent = stored;
} else {
// Different inputs, so the payload, the flags and the values go
// and the snippets run again on this page view. A publisher who
// puts a cache buster in the script URL lands it in the rendered
// parameters and so invalidates on every page view, which is
// accepted because such a parameter already defeats the browser
// cache on the same response.
clearCache();
}
};
// 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 started = 0;
var cachedJson = getCachedJson();
// Held for the duration of the loop so that a snippet which invokes its
// callback synchronously cannot take the counter to zero, and dispatch a
// request, while snippets later in the list have not been run yet.
callbackCounter++;
// 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) {
// The stores a snippet makes, found in its text. Group 3 is the
// name of a quoted store and group 6 the name of a template
// literal store, and groups 4 and 7 start their values. A quoted
// name is a fixed start, optionally joined with + to further
// quoted text and to plain variable names, as in
// "51D_Pos_" + key + "=" and "51D_Bandwidth" + "=". Group 3 then
// holds the text between the outer quotes, so the rewrite below
// puts the same join inside the session storage key and the
// snippet builds the same name there as it would for a cookie.
// Any other form, such as a name joined to a member or a call,
// or a template literal with anything but a fixed name and one
// value, is not matched, and its store is left as it is rather
// than rewritten under a name the pattern did not understand.
let valueSetPrefix = new RegExp('document\\.cookie\\s*=\\s*(("([A-Za-z0-9_]+(?:"\\s*\\+\\s*(?:[A-Za-z_$][A-Za-z0-9_$]*\\s*\\+\\s*)*"[A-Za-z0-9_]*)*)\\s*=\\s*"\\s*\\+\\s*([^\\s};]+))|(`([A-Za-z0-9_]+)\\s*=\\s*\\$\\{([^}]+)\\}`))', 'g');
let session51DataPrefix = sessionKey + "_data_";
{{^_enableCookies}}
let sessionSetPatch = 'window.sessionStorage["' + session51DataPrefix + '$3$6"]=$4$7';
{{/_enableCookies}}
// Store an empty result for each value a snippet sets that has
// none yet. An empty value is how the server is told the snippet
// ran and stored nothing, which is the only report it gets from a
// snippet whose interface is missing at run time, and without one
// the page waits for a result that never comes. Groups 3 and 6 of
// the pattern the rewrite already uses hold the name. The empty
// result goes to session storage whatever the cookie setting, so
// the script gains no cookie write of its own, and
// getFodSavedValues falls back to that result for a name the
// cookies do not carry. A quoted name joined only to further
// quoted text is one fixed name once the joins are removed. A
// quote left after that means a variable is joined in, so the
// name is only known as the snippet runs and nothing is stored
// for it here, because storing the text of the join would send
// the server a name no snippet writes.
let storeEmptyValues = function(snippet) {
let found;
valueSetPrefix.lastIndex = 0;
while ((found = valueSetPrefix.exec(snippet)) !== null) {
let valueName = found[6];
if (found[3]) {
valueName = found[3].replace(/"\s*\+\s*"/g, '');
if (valueName.indexOf('"') !== -1) {
continue;
}
}
if (!valueName) {
continue;
}
try {
if (typeof window !== 'undefined' &&
window.sessionStorage &&
window.sessionStorage.getItem(
session51DataPrefix + valueName) === null) {
window.sessionStorage[
session51DataPrefix + valueName] = "";
}
} catch (err) {
// Storage is blocked, so nothing is stored for it.
}
}
};
// 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);
var isCached = cachedJson &&
sessionStorage.getItem(sessionKey + "_property_" + name);
// If the property has already been processed then skip it.
if (isCached) {
// Record it as started so that hasJSFunctions agrees this
// snippet will not run in this page view. If it disagreed,
// loadParsedJSON would hand back to a call to process that
// does nothing and returns to loadParsedJSON again.
jsPropertiesStarted.push(name);
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++;
// Before the snippet runs, so a value the snippet stores
// replaces the empty one, and before the request is built,
// so the empty one is in the record of that request's
// inputs and the next page view computes the same record
// and keeps its cache.
storeEmptyValues(body);
{{^_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);
} else {
func = new Function(
"try {\n" +
body + "\n" +
"} catch (err) {\n" +
"console.log(err);" +
"}"
);
func();