@ecomconsult/consentkit 0.3.5 → 0.5.0
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.
- package/README.md +262 -20
- package/npm/core.mjs +4 -1
- package/npm/index.cjs +4 -1
- package/npm/index.d.ts +228 -2
- package/npm/index.mjs +14 -2
- package/npm/internal-stub.mjs +12 -1
- package/package.json +1 -1
- package/src/ck-core.js +524 -38
- package/src/ck-debug.js +137 -3
- package/src/ck-saas.js +36 -1
- package/src/ck-ui-branding.js +511 -0
- package/src/ck-ui.js +530 -457
package/src/ck-core.js
CHANGED
|
@@ -22,6 +22,13 @@
|
|
|
22
22
|
nativeScriptSrcDesc = Object.getOwnPropertyDescriptor(global.HTMLScriptElement.prototype, 'src');
|
|
23
23
|
}
|
|
24
24
|
} catch (e) { /* noop */ }
|
|
25
|
+
var nativeIframeSrcDesc = null;
|
|
26
|
+
try {
|
|
27
|
+
if (global.HTMLIFrameElement && global.HTMLIFrameElement.prototype) {
|
|
28
|
+
nativeIframeSrcDesc = Object.getOwnPropertyDescriptor(global.HTMLIFrameElement.prototype, 'src');
|
|
29
|
+
}
|
|
30
|
+
} catch (e) { /* noop */ }
|
|
31
|
+
var nativeRemoveAttribute = (global.Element && global.Element.prototype && global.Element.prototype.removeAttribute) || null;
|
|
25
32
|
|
|
26
33
|
// Internal flag: while true, patches let everything through (used when we
|
|
27
34
|
// re-create previously blocked scripts after consent).
|
|
@@ -70,6 +77,10 @@
|
|
|
70
77
|
'nr-data.net': 'analytics', // New Relic beacon
|
|
71
78
|
'datadoghq.com': 'analytics', // RUM
|
|
72
79
|
'datadoghq-browser-agent.com': 'analytics',
|
|
80
|
+
// Cloudflare Web Analytics. Subdomain only, and deliberately NOT in
|
|
81
|
+
// INFRA_DB: the rest of Cloudflare's edge is infrastructure, but this one
|
|
82
|
+
// beacon is a measurement product (§8 names it by hand for that reason).
|
|
83
|
+
'static.cloudflareinsights.com': 'analytics',
|
|
73
84
|
|
|
74
85
|
// --- marketing -----------------------------------------------------
|
|
75
86
|
'connect.facebook.net': 'marketing',
|
|
@@ -115,6 +126,15 @@
|
|
|
115
126
|
'tidiochat.com': 'functional'
|
|
116
127
|
};
|
|
117
128
|
|
|
129
|
+
// Runtime overrides fed in by ConsentKit._extendHostDb(map) — the SaaS
|
|
130
|
+
// config's `hostdb`, or a hand call on a standalone page. Kept SEPARATE from
|
|
131
|
+
// HOST_DB on purpose: tools/export-hostdb.mjs extracts the HOST_DB literal
|
|
132
|
+
// out of this file with node:vm, and tools/sync-hostdb.mjs writes into that
|
|
133
|
+
// same literal. A runtime map merged into HOST_DB would be invisible to both
|
|
134
|
+
// (it never exists on disk) yet would blur what "the shipped database" means.
|
|
135
|
+
// Consulted BEFORE HOST_DB, so an override wins over a built-in entry.
|
|
136
|
+
var EXTRA_DB = {};
|
|
137
|
+
|
|
118
138
|
// Path-fragment database: URL substring -> category. Matched against the full
|
|
119
139
|
// resolved URL, so it can distinguish two very different scripts served from
|
|
120
140
|
// the SAME host (googletagmanager.com).
|
|
@@ -199,7 +219,16 @@
|
|
|
199
219
|
policyVersion: '1',
|
|
200
220
|
language: 'auto',
|
|
201
221
|
layout: { type: 'bar', position: 'bottom' },
|
|
202
|
-
|
|
222
|
+
// 0.5.0: radius/font/buttons are resolved in ck-ui.js, not defaulted here.
|
|
223
|
+
// A concrete default in this object would be merged into every config and
|
|
224
|
+
// would then shadow the UI's own defaults — which is exactly what happened
|
|
225
|
+
// to `radius: '10px'` before 0.5.0: it pinned the pre-reference geometry on
|
|
226
|
+
// every site whether or not the owner had ever set it.
|
|
227
|
+
theme: { accent: '#2B50D8' },
|
|
228
|
+
// 0.5.0 (SPEC V1.6 §2). `detailsAction` is deliberately absent rather than
|
|
229
|
+
// set: its default depends on whether policyUrl is present, and a merged
|
|
230
|
+
// concrete value here would make that URL-sensitive default unreachable.
|
|
231
|
+
texts: {},
|
|
203
232
|
categories: {
|
|
204
233
|
functional: { enabled: true },
|
|
205
234
|
analytics: { enabled: true },
|
|
@@ -207,9 +236,111 @@
|
|
|
207
236
|
},
|
|
208
237
|
consentTtlDays: 365,
|
|
209
238
|
integrations: { gcm: true, gtmDataLayer: true },
|
|
239
|
+
// 'known' — block what the tracker database recognises (the default, and
|
|
240
|
+
// everything ConsentKit did before 0.4.0).
|
|
241
|
+
// 'strict' — additionally hold back EVERY third-party script/iframe that is
|
|
242
|
+
// not same-site, not in `allow` and not in BASE_ALLOW.
|
|
243
|
+
blocking: { mode: 'known', allow: [] },
|
|
210
244
|
cookieTable: []
|
|
211
245
|
};
|
|
212
246
|
|
|
247
|
+
// Infrastructure (§8) — NOT a consent category, a CLASS of host.
|
|
248
|
+
//
|
|
249
|
+
// These are the CDNs, static hosts and font services that site builders and
|
|
250
|
+
// hosting platforms serve a site's OWN markup, styles and scripts from. A
|
|
251
|
+
// Tilda page loads its own layout from tildacdn.com; a Wix page loads its own
|
|
252
|
+
// from parastorage.com. Reporting those to the site owner as «сторонние
|
|
253
|
+
// подключения без категории» is noise about something they cannot decide:
|
|
254
|
+
// there is no consent question to answer and no switch to flip. §8 exists to
|
|
255
|
+
// stop the audit spending the owner's attention on them.
|
|
256
|
+
//
|
|
257
|
+
// Two consequences, both of them the point:
|
|
258
|
+
// - strict mode NEVER intercepts these (a builder's dynamic modules break
|
|
259
|
+
// without their own CDN, and blocking them breaks the site, not a tracker);
|
|
260
|
+
// - the scanner flags them `infra: true`, keeps them out of the summary and
|
|
261
|
+
// never writes them to tracker_observations.
|
|
262
|
+
//
|
|
263
|
+
// Suffix-matched, exactly like HOST_DB. Membership here is not a claim that a
|
|
264
|
+
// host is harmless in general — it is a claim that it serves the site's own
|
|
265
|
+
// assets. Anything that MEASURES belongs in HOST_DB with a real category
|
|
266
|
+
// instead: static.cloudflareinsights.com is the worked example (§8 calls it
|
|
267
|
+
// out by name), and it sits above as `analytics` for that reason. Because
|
|
268
|
+
// categoryForUrl is consulted before the strict allowlist, a host in both
|
|
269
|
+
// places is classified, not waved through — so a bare `cloudflare.com` here
|
|
270
|
+
// would be a bug.
|
|
271
|
+
var INFRA_DB = [
|
|
272
|
+
// --- site builders and hosting platforms ---------------------------
|
|
273
|
+
'tildacdn.com',
|
|
274
|
+
'tildacdn.net',
|
|
275
|
+
'tilda.ws',
|
|
276
|
+
'static.wixstatic.com',
|
|
277
|
+
'parastorage.com', // Wix static assets
|
|
278
|
+
'cdn.shopify.com',
|
|
279
|
+
'squarespace-cdn.com',
|
|
280
|
+
'assets.website-files.com', // Webflow
|
|
281
|
+
// --- general asset CDNs --------------------------------------------
|
|
282
|
+
'cdn.jsdelivr.net',
|
|
283
|
+
'unpkg.com',
|
|
284
|
+
'cdnjs.cloudflare.com',
|
|
285
|
+
'code.jquery.com',
|
|
286
|
+
'ajax.googleapis.com',
|
|
287
|
+
// --- fonts ----------------------------------------------------------
|
|
288
|
+
'fonts.googleapis.com',
|
|
289
|
+
'fonts.gstatic.com',
|
|
290
|
+
// --- captcha ---------------------------------------------------------
|
|
291
|
+
// A page whose form is gated behind a captcha is unusable without it. The
|
|
292
|
+
// Google-hosted halves are path-scoped in BASE_ALLOW_PATH below, since
|
|
293
|
+
// www.google.com and www.gstatic.com cannot be waved through wholesale.
|
|
294
|
+
'hcaptcha.com',
|
|
295
|
+
// --- our own service -------------------------------------------------
|
|
296
|
+
// The consent tool must not report itself as an unnamed third party.
|
|
297
|
+
'consent.ecomconsult.net'
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
// Hosts strict mode never intercepts that are NOT infrastructure: things a
|
|
301
|
+
// page is broken or unusable without, but which are somebody's product
|
|
302
|
+
// rather than the site's own asset delivery. Kept separate from INFRA_DB so
|
|
303
|
+
// that «инфраструктура» in a report means what §8 says it means — a payment
|
|
304
|
+
// form is a third party the owner chose, and calling it infrastructure would
|
|
305
|
+
// hide a real decision. Both lists feed strict mode identically.
|
|
306
|
+
var BASE_ALLOW = [
|
|
307
|
+
'js.stripe.com',
|
|
308
|
+
'pay.google.com',
|
|
309
|
+
'checkout.creem.io'
|
|
310
|
+
];
|
|
311
|
+
|
|
312
|
+
// Path-scoped members of the base allowlist: allowed only on this exact path
|
|
313
|
+
// prefix, because the host at large is not something to wave through.
|
|
314
|
+
//
|
|
315
|
+
// §8 lists these two under infrastructure, and they are — but they cannot
|
|
316
|
+
// live in INFRA_DB, which is a flat list of suffix-matched HOSTS. Allowing
|
|
317
|
+
// `www.google.com` as a host would wave through every Google property, and
|
|
318
|
+
// `_isInfra(host)` has no path to test. So they stay here: strict mode treats
|
|
319
|
+
// them exactly as it treats INFRA_DB, and `_infra()` reports hosts only.
|
|
320
|
+
var BASE_ALLOW_PATH = [
|
|
321
|
+
{ host: 'www.google.com', path: '/recaptcha' },
|
|
322
|
+
{ host: 'www.gstatic.com', path: '/recaptcha' }
|
|
323
|
+
];
|
|
324
|
+
|
|
325
|
+
// Multi-label public suffixes: without these, `bbc.co.uk` and `itv.co.uk`
|
|
326
|
+
// would share the registrable domain `co.uk` and count as same-site. The list
|
|
327
|
+
// only ever WIDENS the registrable domain (two labels -> three), so a missing
|
|
328
|
+
// entry can only make strict mode treat a sibling as first-party and let it
|
|
329
|
+
// through — never make it block a genuine first-party asset. A full public
|
|
330
|
+
// suffix list is ~10k entries and has no place in a zero-dependency client.
|
|
331
|
+
var PSL_TWO_LABEL = {
|
|
332
|
+
'co.uk': 1, 'org.uk': 1, 'me.uk': 1, 'ac.uk': 1, 'gov.uk': 1, 'net.uk': 1, 'sch.uk': 1,
|
|
333
|
+
'com.au': 1, 'net.au': 1, 'org.au': 1, 'edu.au': 1, 'gov.au': 1,
|
|
334
|
+
'co.nz': 1, 'net.nz': 1, 'org.nz': 1,
|
|
335
|
+
'com.br': 1, 'net.br': 1, 'org.br': 1,
|
|
336
|
+
'co.jp': 1, 'ne.jp': 1, 'or.jp': 1, 'ac.jp': 1,
|
|
337
|
+
'co.za': 1, 'org.za': 1,
|
|
338
|
+
'com.cn': 1, 'net.cn': 1, 'org.cn': 1,
|
|
339
|
+
'co.in': 1, 'net.in': 1, 'org.in': 1,
|
|
340
|
+
'com.tr': 1, 'com.mx': 1, 'com.ar': 1, 'com.sg': 1, 'com.hk': 1,
|
|
341
|
+
'com.ua': 1, 'com.pl': 1, 'com.ru': 1, 'co.il': 1, 'co.kr': 1
|
|
342
|
+
};
|
|
343
|
+
|
|
213
344
|
// ---------------------------------------------------------------------------
|
|
214
345
|
// Small utilities (all defensive — core must never throw)
|
|
215
346
|
// ---------------------------------------------------------------------------
|
|
@@ -585,9 +716,25 @@
|
|
|
585
716
|
// ---------------------------------------------------------------------------
|
|
586
717
|
// Blocking engine — URL classification
|
|
587
718
|
// ---------------------------------------------------------------------------
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
719
|
+
// host === key, or host is a subdomain of key. The one matching rule in the
|
|
720
|
+
// engine: HOST_DB, EXTRA_DB and BASE_ALLOW all use it.
|
|
721
|
+
function hostMatches(host, key) {
|
|
722
|
+
if (!host || !key) { return false; }
|
|
723
|
+
return host === key || (host.length > key.length && host.slice(-(key.length + 1)) === '.' + key);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function lookupHostMap(map, host) {
|
|
727
|
+
var keys = Object.keys(map);
|
|
728
|
+
for (var i = 0; i < keys.length; i++) {
|
|
729
|
+
if (hostMatches(host, keys[i])) { return map[keys[i]]; }
|
|
730
|
+
}
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Splits a URL into { host, url } with the page as the resolution base, so a
|
|
735
|
+
// relative src resolves to the first-party host rather than to nothing.
|
|
736
|
+
function urlParts(src) {
|
|
737
|
+
var s = String(src);
|
|
591
738
|
var host = '';
|
|
592
739
|
try {
|
|
593
740
|
var base = (global.location && global.location.href) || 'http://localhost/';
|
|
@@ -595,19 +742,25 @@
|
|
|
595
742
|
host = (u.hostname || '').toLowerCase();
|
|
596
743
|
s = u.href;
|
|
597
744
|
} catch (e) {
|
|
598
|
-
var m = /^(?:[a-z]+:)?\/\/([^/?#]+)/i.exec(src);
|
|
745
|
+
var m = /^(?:[a-z]+:)?\/\/([^/?#]+)/i.exec(String(src));
|
|
599
746
|
host = m ? m[1].toLowerCase().replace(/:\d+$/, '') : '';
|
|
600
747
|
}
|
|
748
|
+
return { host: host, url: s };
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function categoryForUrl(src) {
|
|
752
|
+
if (!src || typeof src !== 'string') { return null; }
|
|
753
|
+
var parts = urlParts(src);
|
|
754
|
+
var host = parts.host;
|
|
601
755
|
if (host) {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
}
|
|
756
|
+
// Service overrides first: an override exists precisely to correct or
|
|
757
|
+
// extend what the shipped table says about a host.
|
|
758
|
+
var over = lookupHostMap(EXTRA_DB, host);
|
|
759
|
+
if (over) { return over; }
|
|
760
|
+
var built = lookupHostMap(HOST_DB, host);
|
|
761
|
+
if (built) { return built; }
|
|
609
762
|
}
|
|
610
|
-
var low = String(
|
|
763
|
+
var low = String(parts.url).toLowerCase();
|
|
611
764
|
var pkeys = Object.keys(PATH_DB);
|
|
612
765
|
for (var j = 0; j < pkeys.length; j++) {
|
|
613
766
|
if (low.indexOf(pkeys[j]) > -1) { return PATH_DB[pkeys[j]]; }
|
|
@@ -615,18 +768,164 @@
|
|
|
615
768
|
return null;
|
|
616
769
|
}
|
|
617
770
|
|
|
771
|
+
// Merges { host: category } into the runtime database. Safe before and after
|
|
772
|
+
// init(): after init nothing already inserted is re-evaluated (a script that
|
|
773
|
+
// has loaded cannot be unloaded), but every later insertion sees the new map.
|
|
774
|
+
function extendHostDb(map) {
|
|
775
|
+
var added = 0;
|
|
776
|
+
try {
|
|
777
|
+
if (!isPlainObject(map)) { return 0; }
|
|
778
|
+
Object.keys(map).forEach(function (rawHost) {
|
|
779
|
+
// The map arrives over the network in the SaaS path: validate both
|
|
780
|
+
// halves rather than trusting the server's shape.
|
|
781
|
+
if (typeof rawHost !== 'string') { return; }
|
|
782
|
+
var host = rawHost.trim().toLowerCase().replace(/:\d+$/, '').replace(/^\.+|\.+$/g, '');
|
|
783
|
+
if (!host || host.indexOf('.') === -1 || /[^a-z0-9.\-]/.test(host)) { return; }
|
|
784
|
+
var cat = map[rawHost];
|
|
785
|
+
if (typeof cat !== 'string' || CATEGORIES.indexOf(cat) === -1) { return; }
|
|
786
|
+
if (EXTRA_DB[host] === cat) { return; }
|
|
787
|
+
EXTRA_DB[host] = cat;
|
|
788
|
+
added++;
|
|
789
|
+
});
|
|
790
|
+
} catch (e) { /* noop */ }
|
|
791
|
+
return added;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// ---------------------------------------------------------------------------
|
|
795
|
+
// Blocking engine — strict mode (§2)
|
|
796
|
+
// ---------------------------------------------------------------------------
|
|
797
|
+
// Everything strict intercepts is filed under the strictest category, so it
|
|
798
|
+
// is released only by a consent that covers marketing.
|
|
799
|
+
var STRICT_CATEGORY = 'marketing';
|
|
800
|
+
|
|
801
|
+
// Registrable domain, best effort: last two labels, or last three when the
|
|
802
|
+
// last two form a known multi-label public suffix.
|
|
803
|
+
function registrable(host) {
|
|
804
|
+
if (!host) { return ''; }
|
|
805
|
+
var labels = String(host).split('.');
|
|
806
|
+
if (labels.length <= 2) { return host; }
|
|
807
|
+
var lastTwo = labels.slice(-2).join('.');
|
|
808
|
+
if (PSL_TWO_LABEL[lastTwo] && labels.length >= 3) { return labels.slice(-3).join('.'); }
|
|
809
|
+
return lastTwo;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Conservative on purpose: whenever the answer is not clearly "third party",
|
|
813
|
+
// this says same-site. A wrong "third party" verdict breaks a live site in
|
|
814
|
+
// strict mode; a wrong "same-site" verdict merely lets one unknown script
|
|
815
|
+
// through, which is exactly what every version before 0.4.0 did.
|
|
816
|
+
function isSameSite(host) {
|
|
817
|
+
if (!host) { return true; } // unparseable -> do not intercept
|
|
818
|
+
var page = hostname().toLowerCase();
|
|
819
|
+
if (!page) { return true; } // no location (SSR/about:blank) -> strict is inert
|
|
820
|
+
if (host === page) { return true; }
|
|
821
|
+
var r = registrable(host);
|
|
822
|
+
var pr = registrable(page);
|
|
823
|
+
return !!r && r === pr;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Is this host infrastructure (§8)? Suffix-matched, like everything else.
|
|
827
|
+
//
|
|
828
|
+
// `ConsentKitDebugUrl` is resolved HERE rather than being baked into
|
|
829
|
+
// INFRA_DB, because it is a runtime window global a site sets to point the
|
|
830
|
+
// debug panel at its own mirror. Reading it at call time is the only way it
|
|
831
|
+
// can be covered at all; a snapshot taken when this file was evaluated would
|
|
832
|
+
// miss every page that sets it after the core loads.
|
|
833
|
+
function isInfraHost(host) {
|
|
834
|
+
if (!host) { return false; }
|
|
835
|
+
for (var i = 0; i < INFRA_DB.length; i++) {
|
|
836
|
+
if (hostMatches(host, INFRA_DB[i])) { return true; }
|
|
837
|
+
}
|
|
838
|
+
try {
|
|
839
|
+
var dbg = global.ConsentKitDebugUrl;
|
|
840
|
+
if (typeof dbg === 'string' && dbg) {
|
|
841
|
+
var dh = urlParts(dbg).host;
|
|
842
|
+
if (dh && dh === host) { return true; }
|
|
843
|
+
}
|
|
844
|
+
} catch (e) { /* noop */ }
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function baseAllowed(host, url) {
|
|
849
|
+
// §8: infrastructure is never intercepted by strict mode. Checked first —
|
|
850
|
+
// it is the larger list and the commoner case on a builder-hosted site.
|
|
851
|
+
if (isInfraHost(host)) { return true; }
|
|
852
|
+
for (var i = 0; i < BASE_ALLOW.length; i++) {
|
|
853
|
+
if (hostMatches(host, BASE_ALLOW[i])) { return true; }
|
|
854
|
+
}
|
|
855
|
+
for (var j = 0; j < BASE_ALLOW_PATH.length; j++) {
|
|
856
|
+
var e = BASE_ALLOW_PATH[j];
|
|
857
|
+
if (host !== e.host) { continue; }
|
|
858
|
+
var path = '';
|
|
859
|
+
try { path = new URL(String(url), 'http://localhost/').pathname || ''; } catch (e2) { path = ''; }
|
|
860
|
+
if (path.indexOf(e.path) === 0) { return true; }
|
|
861
|
+
}
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function siteAllowed(host) {
|
|
866
|
+
try {
|
|
867
|
+
var list = config.blocking && config.blocking.allow;
|
|
868
|
+
if (!list || typeof list.length !== 'number') { return false; }
|
|
869
|
+
for (var i = 0; i < list.length; i++) {
|
|
870
|
+
var entry = list[i];
|
|
871
|
+
if (typeof entry !== 'string') { continue; }
|
|
872
|
+
var key = entry.trim().toLowerCase().replace(/^\.+/, '');
|
|
873
|
+
if (key && hostMatches(host, key)) { return true; }
|
|
874
|
+
}
|
|
875
|
+
} catch (e) { /* noop */ }
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function strictMode() {
|
|
880
|
+
try { return !!(config.blocking && config.blocking.mode === 'strict'); } catch (e) { return false; }
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// True when strict mode should hold this URL back. Reached only for URLs the
|
|
884
|
+
// tracker database does NOT recognise: a known host has a real category and
|
|
885
|
+
// is decided by allowed() long before this runs, which is why a HOST_DB
|
|
886
|
+
// `necessary` or `functional` host passes strict without a special case.
|
|
887
|
+
function strictBlocks(src) {
|
|
888
|
+
if (!strictMode()) { return false; }
|
|
889
|
+
var s = String(src || '');
|
|
890
|
+
// Non-network schemes carry no third party. A bare "//host/x" has no scheme
|
|
891
|
+
// and is protocol-relative, so it is deliberately not caught here.
|
|
892
|
+
if (/^\s*(?:data|blob|javascript|about|mailto|tel):/i.test(s)) { return false; }
|
|
893
|
+
var parts = urlParts(s);
|
|
894
|
+
var host = parts.host;
|
|
895
|
+
if (!host) { return false; }
|
|
896
|
+
if (isSameSite(host)) { return false; }
|
|
897
|
+
if (baseAllowed(host, parts.url)) { return false; }
|
|
898
|
+
if (siteAllowed(host)) { return false; }
|
|
899
|
+
// Intercepted unknowns are treated as marketing — the strictest category —
|
|
900
|
+
// so they come back only when the visitor accepts marketing.
|
|
901
|
+
return !allowed(STRICT_CATEGORY);
|
|
902
|
+
}
|
|
903
|
+
|
|
618
904
|
function allowed(cat) {
|
|
619
905
|
if (cat === 'necessary' || !cat) { return true; }
|
|
620
906
|
if (CATEGORIES.indexOf(cat) === -1) { return true; }
|
|
621
907
|
return state.categories[cat] === true;
|
|
622
908
|
}
|
|
623
909
|
|
|
624
|
-
// True when the URL
|
|
910
|
+
// True when the URL must be held back: a known tracker whose category is not
|
|
911
|
+
// yet granted, or — in strict mode — an unknown third party.
|
|
625
912
|
function shouldBlock(src) {
|
|
626
913
|
if (bypass) { return false; }
|
|
627
914
|
var cat = categoryForUrl(src);
|
|
628
|
-
if (
|
|
629
|
-
return
|
|
915
|
+
if (cat) { return !allowed(cat); }
|
|
916
|
+
return strictBlocks(src);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// The category an interception is filed under. Known hosts keep their own;
|
|
920
|
+
// a strict interception is marketing.
|
|
921
|
+
function blockCategory(src) {
|
|
922
|
+
return categoryForUrl(src) || STRICT_CATEGORY;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Was this particular interception a strict-mode one (i.e. the URL is not in
|
|
926
|
+
// the tracker database at all)? Drives the «strict» label in the debug panel.
|
|
927
|
+
function isStrictHit(src) {
|
|
928
|
+
return !categoryForUrl(src) && strictMode();
|
|
630
929
|
}
|
|
631
930
|
|
|
632
931
|
// Registry of everything the engine intercepted, for the debug panel (§8.1
|
|
@@ -660,7 +959,7 @@
|
|
|
660
959
|
return { host: host, path: path };
|
|
661
960
|
}
|
|
662
961
|
|
|
663
|
-
function noteBlocked(el, src, cat, origin) {
|
|
962
|
+
function noteBlocked(el, src, cat, origin, strict) {
|
|
664
963
|
try {
|
|
665
964
|
if (blockedLog.length >= BLOCKED_MAX) { return; }
|
|
666
965
|
var kind = 'script';
|
|
@@ -678,13 +977,18 @@
|
|
|
678
977
|
path: parts.path,
|
|
679
978
|
kind: kind,
|
|
680
979
|
category: cat || null,
|
|
681
|
-
origin: origin || 'engine'
|
|
980
|
+
origin: origin || 'engine',
|
|
981
|
+
strict: strict === true,
|
|
982
|
+
// Flipped by noteRevived() when applyConsentToDom() actually brings the
|
|
983
|
+
// element back. An entry that never flips is one the visitor consented
|
|
984
|
+
// to and that still did not load — worth showing in the debug panel.
|
|
985
|
+
revived: false
|
|
682
986
|
});
|
|
683
987
|
} catch (e) { /* noop */ }
|
|
684
988
|
}
|
|
685
989
|
|
|
686
990
|
function markBlocked(el, src, cat) {
|
|
687
|
-
noteBlocked(el, src, cat, 'engine');
|
|
991
|
+
noteBlocked(el, src, cat, 'engine', isStrictHit(src));
|
|
688
992
|
try {
|
|
689
993
|
if (nativeSetAttribute) {
|
|
690
994
|
nativeSetAttribute.call(el, 'data-ck-blocked', '1');
|
|
@@ -702,6 +1006,36 @@
|
|
|
702
1006
|
} catch (e) { /* noop */ }
|
|
703
1007
|
}
|
|
704
1008
|
|
|
1009
|
+
// An intercepted iframe is left in EXACTLY the shape applyConsentToDom()
|
|
1010
|
+
// already revives — data-ck + data-src and NO src attribute — so revival is
|
|
1011
|
+
// the one code path for hand-marked, plugin-rewritten and engine-blocked
|
|
1012
|
+
// iframes alike. `type="text/plain"` is script-only and must not be set here.
|
|
1013
|
+
// data-ck-blocked also keeps _blocked()'s markup sweep from listing this
|
|
1014
|
+
// element a second time: it matches iframe[data-ck][data-src] too.
|
|
1015
|
+
// Flags the registry entry for this URL+kind as revived, so _blocked() can
|
|
1016
|
+
// tell "came back after consent" from "was intercepted and never returned".
|
|
1017
|
+
function noteRevived(src, kind) {
|
|
1018
|
+
try {
|
|
1019
|
+
var parts = safeUrlParts(src);
|
|
1020
|
+
for (var i = 0; i < blockedLog.length; i++) {
|
|
1021
|
+
var b = blockedLog[i];
|
|
1022
|
+
if (b.host === parts.host && b.path === parts.path && b.kind === kind) { b.revived = true; }
|
|
1023
|
+
}
|
|
1024
|
+
} catch (e) { /* noop */ }
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function markBlockedIframe(el, src, cat) {
|
|
1028
|
+
noteBlocked(el, src, cat, 'engine', isStrictHit(src));
|
|
1029
|
+
try {
|
|
1030
|
+
if (!nativeSetAttribute) { return; }
|
|
1031
|
+
nativeSetAttribute.call(el, 'data-ck-blocked', '1');
|
|
1032
|
+
nativeSetAttribute.call(el, 'data-src', src);
|
|
1033
|
+
nativeSetAttribute.call(el, 'data-ck', cat || STRICT_CATEGORY);
|
|
1034
|
+
// Any src already on the element must go, or revival skips it.
|
|
1035
|
+
try { if (nativeRemoveAttribute) { nativeRemoveAttribute.call(el, 'src'); } } catch (e2) { /* noop */ }
|
|
1036
|
+
} catch (e) { /* noop */ }
|
|
1037
|
+
}
|
|
1038
|
+
|
|
705
1039
|
// ---------------------------------------------------------------------------
|
|
706
1040
|
// Blocking engine — patches (installed at parse time)
|
|
707
1041
|
// ---------------------------------------------------------------------------
|
|
@@ -726,16 +1060,44 @@
|
|
|
726
1060
|
}
|
|
727
1061
|
} catch (e) { /* noop */ }
|
|
728
1062
|
|
|
729
|
-
//
|
|
1063
|
+
// 1b. HTMLIFrameElement.prototype.src setter. Strict mode holds back
|
|
1064
|
+
// third-party frames too (§2), and a known tracker embedded as an iframe
|
|
1065
|
+
// was never caught before either.
|
|
1066
|
+
try {
|
|
1067
|
+
if (nativeIframeSrcDesc && nativeIframeSrcDesc.set && nativeIframeSrcDesc.configurable !== false) {
|
|
1068
|
+
Object.defineProperty(global.HTMLIFrameElement.prototype, 'src', {
|
|
1069
|
+
configurable: true,
|
|
1070
|
+
enumerable: nativeIframeSrcDesc.enumerable,
|
|
1071
|
+
get: function () {
|
|
1072
|
+
try { return nativeIframeSrcDesc.get.call(this); } catch (e) { return ''; }
|
|
1073
|
+
},
|
|
1074
|
+
set: function (v) {
|
|
1075
|
+
if (shouldBlock(v)) {
|
|
1076
|
+
markBlockedIframe(this, String(v), blockCategory(v));
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
try { nativeIframeSrcDesc.set.call(this, v); } catch (e) { /* noop */ }
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
} catch (e) { /* noop */ }
|
|
1084
|
+
|
|
1085
|
+
// 2. Element.prototype.setAttribute — covers setAttribute('src', ...) on
|
|
1086
|
+
// both scripts and iframes.
|
|
730
1087
|
try {
|
|
731
1088
|
if (nativeSetAttribute && global.Element && global.Element.prototype) {
|
|
732
1089
|
global.Element.prototype.setAttribute = function (name, value) {
|
|
733
1090
|
try {
|
|
734
|
-
if (!bypass && typeof name === 'string' && name.toLowerCase() === 'src' &&
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
1091
|
+
if (!bypass && typeof name === 'string' && name.toLowerCase() === 'src' && this && this.tagName) {
|
|
1092
|
+
var t = String(this.tagName).toUpperCase();
|
|
1093
|
+
if ((t === 'SCRIPT' || t === 'IFRAME') && shouldBlock(value)) {
|
|
1094
|
+
if (t === 'IFRAME') {
|
|
1095
|
+
markBlockedIframe(this, String(value), blockCategory(value));
|
|
1096
|
+
} else {
|
|
1097
|
+
markBlocked(this, String(value), blockCategory(value));
|
|
1098
|
+
}
|
|
1099
|
+
return undefined;
|
|
1100
|
+
}
|
|
739
1101
|
}
|
|
740
1102
|
} catch (e) { /* fall through to native */ }
|
|
741
1103
|
return nativeSetAttribute.apply(this, arguments);
|
|
@@ -749,10 +1111,10 @@
|
|
|
749
1111
|
doc.createElement = function (tag) {
|
|
750
1112
|
var el = nativeCreateElement.apply(null, arguments);
|
|
751
1113
|
try {
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
1114
|
+
var name = (!bypass && typeof tag === 'string') ? tag.toLowerCase() : '';
|
|
1115
|
+
// Own-property guard so the element is covered even if the
|
|
1116
|
+
// prototype patch was reverted by another library.
|
|
1117
|
+
if (name === 'script' && nativeScriptSrcDesc && nativeScriptSrcDesc.set) {
|
|
756
1118
|
Object.defineProperty(el, 'src', {
|
|
757
1119
|
configurable: true,
|
|
758
1120
|
enumerable: false,
|
|
@@ -761,12 +1123,27 @@
|
|
|
761
1123
|
},
|
|
762
1124
|
set: function (v) {
|
|
763
1125
|
if (shouldBlock(v)) {
|
|
764
|
-
markBlocked(this, String(v),
|
|
1126
|
+
markBlocked(this, String(v), blockCategory(v));
|
|
765
1127
|
return;
|
|
766
1128
|
}
|
|
767
1129
|
try { nativeScriptSrcDesc.set.call(this, v); } catch (e) { /* noop */ }
|
|
768
1130
|
}
|
|
769
1131
|
});
|
|
1132
|
+
} else if (name === 'iframe' && nativeIframeSrcDesc && nativeIframeSrcDesc.set) {
|
|
1133
|
+
Object.defineProperty(el, 'src', {
|
|
1134
|
+
configurable: true,
|
|
1135
|
+
enumerable: false,
|
|
1136
|
+
get: function () {
|
|
1137
|
+
try { return nativeIframeSrcDesc.get.call(this); } catch (e) { return ''; }
|
|
1138
|
+
},
|
|
1139
|
+
set: function (v) {
|
|
1140
|
+
if (shouldBlock(v)) {
|
|
1141
|
+
markBlockedIframe(this, String(v), blockCategory(v));
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
try { nativeIframeSrcDesc.set.call(this, v); } catch (e) { /* noop */ }
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
770
1147
|
}
|
|
771
1148
|
} catch (e) { /* noop */ }
|
|
772
1149
|
return el;
|
|
@@ -798,13 +1175,30 @@
|
|
|
798
1175
|
if (!node || node.nodeType !== 1) { return; }
|
|
799
1176
|
var tag = node.tagName ? String(node.tagName).toUpperCase() : '';
|
|
800
1177
|
if (tag === 'SCRIPT') { inspectScript(node); }
|
|
1178
|
+
if (tag === 'IFRAME') { inspectIframe(node); }
|
|
801
1179
|
if (typeof node.querySelectorAll === 'function') {
|
|
802
1180
|
var kids = node.querySelectorAll('script');
|
|
803
1181
|
for (var i = 0; i < kids.length; i++) { inspectScript(kids[i]); }
|
|
1182
|
+
var frames = node.querySelectorAll('iframe');
|
|
1183
|
+
for (var j = 0; j < frames.length; j++) { inspectIframe(frames[j]); }
|
|
804
1184
|
}
|
|
805
1185
|
} catch (e) { /* noop */ }
|
|
806
1186
|
}
|
|
807
1187
|
|
|
1188
|
+
// Late net for iframes that arrived as markup. Same honesty as inspectScript:
|
|
1189
|
+
// once the element is connected the request may already be in flight — the
|
|
1190
|
+
// src/createElement patches are the reliable path.
|
|
1191
|
+
function inspectIframe(el) {
|
|
1192
|
+
try {
|
|
1193
|
+
if (!el || el.getAttribute === undefined) { return; }
|
|
1194
|
+
if (el.getAttribute('data-ck-blocked')) { return; }
|
|
1195
|
+
if (el.getAttribute('data-ck') && el.getAttribute('data-src')) { return; } // manual markup
|
|
1196
|
+
var src = el.getAttribute('src');
|
|
1197
|
+
if (!src) { return; }
|
|
1198
|
+
if (shouldBlock(src)) { markBlockedIframe(el, src, blockCategory(src)); }
|
|
1199
|
+
} catch (e) { /* noop */ }
|
|
1200
|
+
}
|
|
1201
|
+
|
|
808
1202
|
function inspectScript(el) {
|
|
809
1203
|
try {
|
|
810
1204
|
if (!el || el.getAttribute === undefined) { return; }
|
|
@@ -814,7 +1208,7 @@
|
|
|
814
1208
|
var src = el.getAttribute('src');
|
|
815
1209
|
if (!src) { return; }
|
|
816
1210
|
if (shouldBlock(src)) {
|
|
817
|
-
var cat =
|
|
1211
|
+
var cat = blockCategory(src);
|
|
818
1212
|
markBlocked(el, src, cat);
|
|
819
1213
|
// Clear the attribute. Note: if the element was already connected the
|
|
820
1214
|
// request may already be in flight — the createElement/src patches are
|
|
@@ -862,6 +1256,7 @@
|
|
|
862
1256
|
try { nativeSetAttribute.call(fresh, 'data-ck-restored', '1'); } catch (e) { /* noop */ }
|
|
863
1257
|
old.parentNode.insertBefore(fresh, old);
|
|
864
1258
|
try { old.parentNode.removeChild(old); } catch (e) { /* noop */ }
|
|
1259
|
+
if (src) { noteRevived(src, 'script'); }
|
|
865
1260
|
} catch (e) {
|
|
866
1261
|
/* noop */
|
|
867
1262
|
} finally {
|
|
@@ -902,6 +1297,7 @@
|
|
|
902
1297
|
var prev = bypass;
|
|
903
1298
|
bypass = true;
|
|
904
1299
|
try { nativeSetAttribute.call(el, 'src', src); } finally { bypass = prev; }
|
|
1300
|
+
noteRevived(src, 'iframe');
|
|
905
1301
|
} catch (e) { /* noop */ }
|
|
906
1302
|
});
|
|
907
1303
|
}
|
|
@@ -909,6 +1305,7 @@
|
|
|
909
1305
|
// Initial sweep for scripts already parsed before the observer attached.
|
|
910
1306
|
function initialScan() {
|
|
911
1307
|
qsa('script[src]').forEach(inspectScript);
|
|
1308
|
+
qsa('iframe[src]').forEach(inspectIframe);
|
|
912
1309
|
}
|
|
913
1310
|
|
|
914
1311
|
// ---------------------------------------------------------------------------
|
|
@@ -1016,7 +1413,7 @@
|
|
|
1016
1413
|
// Public API
|
|
1017
1414
|
// ---------------------------------------------------------------------------
|
|
1018
1415
|
var ConsentKit = {
|
|
1019
|
-
version: '0.
|
|
1416
|
+
version: '0.5.0',
|
|
1020
1417
|
config: config,
|
|
1021
1418
|
|
|
1022
1419
|
init: function (userConfig) {
|
|
@@ -1024,6 +1421,12 @@
|
|
|
1024
1421
|
config = mergeConfig(config, userConfig);
|
|
1025
1422
|
ConsentKit.config = config;
|
|
1026
1423
|
|
|
1424
|
+
// Service / author overrides, merged BEFORE initialScan() below so the
|
|
1425
|
+
// scripts already in the markup are classified against them. In SaaS
|
|
1426
|
+
// mode ck-saas.js has usually applied these already; extendHostDb is
|
|
1427
|
+
// idempotent, so doing it twice costs nothing.
|
|
1428
|
+
if (userConfig && isPlainObject(userConfig.hostdb)) { extendHostDb(userConfig.hostdb); }
|
|
1429
|
+
|
|
1027
1430
|
if (initialized) {
|
|
1028
1431
|
// Idempotent: merge config, no re-restore, no duplicate ck:init.
|
|
1029
1432
|
return publicState();
|
|
@@ -1074,6 +1477,73 @@
|
|
|
1074
1477
|
_categoryForUrl: categoryForUrl,
|
|
1075
1478
|
_categories: CATEGORIES.slice(),
|
|
1076
1479
|
|
|
1480
|
+
// Merges { host: category } into the runtime tracker database (§1.3).
|
|
1481
|
+
// Works before AND after init(): after init nothing already inserted is
|
|
1482
|
+
// re-evaluated — a script that has loaded cannot be unloaded — but every
|
|
1483
|
+
// later insertion is classified against the extended map. Returns the
|
|
1484
|
+
// number of entries actually added; unknown categories and malformed hosts
|
|
1485
|
+
// are dropped, because this map arrives over the network in the SaaS path.
|
|
1486
|
+
_extendHostDb: function (map) {
|
|
1487
|
+
try { return extendHostDb(map); } catch (e) { return 0; }
|
|
1488
|
+
},
|
|
1489
|
+
|
|
1490
|
+
// The built-in strict-mode allowlist, exported for docs and tests so the
|
|
1491
|
+
// list a site owner reads is the list the engine actually uses. Since 0.4.1
|
|
1492
|
+
// that is INFRA_DB (§8) plus the payment/captcha set plus the path-scoped
|
|
1493
|
+
// entries — everything baseAllowed() consults, in one list, because what a
|
|
1494
|
+
// site owner needs to know is «what does strict mode let through», not how
|
|
1495
|
+
// the engine files it internally.
|
|
1496
|
+
//
|
|
1497
|
+
// A GETTER, not a plain array: a plain property is evaluated once, and the
|
|
1498
|
+
// single array it produced would be handed to every caller — one
|
|
1499
|
+
// `ConsentKit._baseAllow.push(...)` from page code would then silently
|
|
1500
|
+
// widen what strict mode lets through for the rest of the page load.
|
|
1501
|
+
// Each read returns a fresh copy, so the list is readable and inert.
|
|
1502
|
+
get _baseAllow() {
|
|
1503
|
+
return INFRA_DB.slice()
|
|
1504
|
+
.concat(BASE_ALLOW)
|
|
1505
|
+
.concat(BASE_ALLOW_PATH.map(function (e) { return e.host + e.path; }));
|
|
1506
|
+
},
|
|
1507
|
+
|
|
1508
|
+
// The infrastructure list (§8): CDNs and static hosts of site builders and
|
|
1509
|
+
// hosting platforms, general asset CDNs, fonts and captcha. A CLASS of
|
|
1510
|
+
// host, not a consent category — nothing here is a decision the site owner
|
|
1511
|
+
// gets to make, which is exactly why the audit stops reporting them.
|
|
1512
|
+
//
|
|
1513
|
+
// Shared with the SaaS scanner, which loads this core and reads this list
|
|
1514
|
+
// to set `thirdParty[].infra`, keep infrastructure out of the summary and
|
|
1515
|
+
// out of tracker_observations. Hosts only: the two path-scoped recaptcha
|
|
1516
|
+
// entries are infrastructure too, but cannot be expressed as bare hosts —
|
|
1517
|
+
// see BASE_ALLOW_PATH. A fresh copy per read, for the same reason as
|
|
1518
|
+
// _baseAllow.
|
|
1519
|
+
_infra: function () {
|
|
1520
|
+
return INFRA_DB.slice();
|
|
1521
|
+
},
|
|
1522
|
+
|
|
1523
|
+
// Is this URL infrastructure? Takes a URL, like every other public
|
|
1524
|
+
// predicate here, so a caller never has to reimplement host extraction.
|
|
1525
|
+
//
|
|
1526
|
+
// A BARE HOST is accepted too ('tildacdn.com'), because the scanner works
|
|
1527
|
+
// in hosts and would otherwise have to glue a fake scheme on every call.
|
|
1528
|
+
// It needs its own branch: urlParts resolves a relative string against the
|
|
1529
|
+
// page, so a bare host arrives as a PATH on the first-party origin and
|
|
1530
|
+
// would answer false. The test is deliberately narrow — a string with no
|
|
1531
|
+
// scheme, no slash, no query and at least one dot is a hostname and cannot
|
|
1532
|
+
// be anything else.
|
|
1533
|
+
_isInfra: function (url) {
|
|
1534
|
+
try {
|
|
1535
|
+
if (!url || typeof url !== 'string') { return false; }
|
|
1536
|
+
var s = url.trim();
|
|
1537
|
+
if (!s) { return false; }
|
|
1538
|
+
if (/^[a-z0-9.\-]+\.[a-z0-9\-]+$/i.test(s)) {
|
|
1539
|
+
return isInfraHost(s.toLowerCase().replace(/^\.+|\.+$/g, ''));
|
|
1540
|
+
}
|
|
1541
|
+
var host = urlParts(s).host;
|
|
1542
|
+
if (!host) { return false; }
|
|
1543
|
+
return isInfraHost(host);
|
|
1544
|
+
} catch (e) { return false; }
|
|
1545
|
+
},
|
|
1546
|
+
|
|
1077
1547
|
// What is being held back until consent: everything the engine intercepted
|
|
1078
1548
|
// (origin 'engine') plus what the site author marked up by hand (origin
|
|
1079
1549
|
// 'markup'), which never goes through markBlocked. Read-only, host+path
|
|
@@ -1090,11 +1560,24 @@
|
|
|
1090
1560
|
try {
|
|
1091
1561
|
for (var i = 0; i < blockedLog.length; i++) {
|
|
1092
1562
|
var b = blockedLog[i];
|
|
1093
|
-
// Interceptions are kept for the life of the page
|
|
1094
|
-
//
|
|
1095
|
-
// held back
|
|
1096
|
-
|
|
1097
|
-
|
|
1563
|
+
// Interceptions are kept for the life of the page. Once the category
|
|
1564
|
+
// is granted the element has USUALLY been revived and is no longer
|
|
1565
|
+
// held back, so reporting it as blocked would be false — but not
|
|
1566
|
+
// always: applyConsentToDom() can only revive an element that is in
|
|
1567
|
+
// the document, and a script created and given a src without ever
|
|
1568
|
+
// being appended stays dead for the life of the page.
|
|
1569
|
+
//
|
|
1570
|
+
// That case is exactly the strict-mode support ticket ("my widget did
|
|
1571
|
+
// not come back after I accepted"), and both README and INSTALL.ru
|
|
1572
|
+
// send the site owner to this panel to diagnose it. So an entry is
|
|
1573
|
+
// dropped only when something on the page actually came back for it;
|
|
1574
|
+
// otherwise it stays, marked `revived: false`.
|
|
1575
|
+
if (allowed(b.category) && b.revived !== false) { continue; }
|
|
1576
|
+
add({
|
|
1577
|
+
host: b.host, path: b.path, kind: b.kind,
|
|
1578
|
+
category: b.category, origin: b.origin, strict: b.strict === true,
|
|
1579
|
+
revived: b.revived !== false
|
|
1580
|
+
});
|
|
1098
1581
|
}
|
|
1099
1582
|
} catch (e) { /* noop */ }
|
|
1100
1583
|
// Hand-marked tags still waiting for their category.
|
|
@@ -1117,7 +1600,10 @@
|
|
|
1117
1600
|
add({
|
|
1118
1601
|
host: parts.host, path: parts.path,
|
|
1119
1602
|
kind: tag === 'iframe' ? 'iframe' : 'script',
|
|
1120
|
-
|
|
1603
|
+
// revived: true means "not applicable" here, not "came back":
|
|
1604
|
+
// this sweep only ever lists hand-marked tags still waiting for
|
|
1605
|
+
// their category, so none of them can be a failed revival.
|
|
1606
|
+
category: cat || null, origin: 'markup', strict: false, revived: true
|
|
1121
1607
|
});
|
|
1122
1608
|
} catch (e2) { /* noop */ }
|
|
1123
1609
|
});
|