@browserless.io/browserless 2.23.0 → 2.24.0-beta-1

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.
Files changed (118) hide show
  1. package/build/routes/chrome/http/pdf.post.body.json +8 -8
  2. package/build/routes/chrome/http/scrape.post.body.json +8 -8
  3. package/build/routes/chrome/http/screenshot.post.body.json +8 -8
  4. package/build/routes/chromium/http/pdf.post.body.json +8 -8
  5. package/build/routes/chromium/http/screenshot.post.body.json +8 -8
  6. package/docker/multi/Dockerfile +2 -1
  7. package/docker/webkit/Dockerfile +2 -1
  8. package/extensions/ublock/_locales/ar/messages.json +5 -1
  9. package/extensions/ublock/_locales/az/messages.json +4 -0
  10. package/extensions/ublock/_locales/be/messages.json +5 -1
  11. package/extensions/ublock/_locales/bg/messages.json +4 -0
  12. package/extensions/ublock/_locales/bn/messages.json +4 -0
  13. package/extensions/ublock/_locales/br_FR/messages.json +4 -0
  14. package/extensions/ublock/_locales/bs/messages.json +4 -0
  15. package/extensions/ublock/_locales/ca/messages.json +4 -0
  16. package/extensions/ublock/_locales/cs/messages.json +6 -2
  17. package/extensions/ublock/_locales/cv/messages.json +4 -0
  18. package/extensions/ublock/_locales/cy/messages.json +15 -11
  19. package/extensions/ublock/_locales/da/messages.json +4 -0
  20. package/extensions/ublock/_locales/de/messages.json +8 -4
  21. package/extensions/ublock/_locales/el/messages.json +4 -0
  22. package/extensions/ublock/_locales/en/messages.json +4 -0
  23. package/extensions/ublock/_locales/en_GB/messages.json +4 -0
  24. package/extensions/ublock/_locales/eo/messages.json +4 -0
  25. package/extensions/ublock/_locales/es/messages.json +5 -1
  26. package/extensions/ublock/_locales/et/messages.json +4 -0
  27. package/extensions/ublock/_locales/eu/messages.json +4 -0
  28. package/extensions/ublock/_locales/fa/messages.json +14 -10
  29. package/extensions/ublock/_locales/fi/messages.json +7 -3
  30. package/extensions/ublock/_locales/fil/messages.json +4 -0
  31. package/extensions/ublock/_locales/fr/messages.json +5 -1
  32. package/extensions/ublock/_locales/fy/messages.json +4 -0
  33. package/extensions/ublock/_locales/gl/messages.json +4 -0
  34. package/extensions/ublock/_locales/gu/messages.json +4 -0
  35. package/extensions/ublock/_locales/he/messages.json +4 -0
  36. package/extensions/ublock/_locales/hi/messages.json +4 -0
  37. package/extensions/ublock/_locales/hr/messages.json +4 -0
  38. package/extensions/ublock/_locales/hu/messages.json +4 -0
  39. package/extensions/ublock/_locales/hy/messages.json +4 -0
  40. package/extensions/ublock/_locales/id/messages.json +4 -0
  41. package/extensions/ublock/_locales/it/messages.json +4 -0
  42. package/extensions/ublock/_locales/ja/messages.json +6 -2
  43. package/extensions/ublock/_locales/ka/messages.json +5 -1
  44. package/extensions/ublock/_locales/kk/messages.json +4 -0
  45. package/extensions/ublock/_locales/kn/messages.json +4 -0
  46. package/extensions/ublock/_locales/ko/messages.json +5 -1
  47. package/extensions/ublock/_locales/lt/messages.json +4 -0
  48. package/extensions/ublock/_locales/lv/messages.json +4 -0
  49. package/extensions/ublock/_locales/mk/messages.json +4 -0
  50. package/extensions/ublock/_locales/ml/messages.json +4 -0
  51. package/extensions/ublock/_locales/mr/messages.json +4 -0
  52. package/extensions/ublock/_locales/ms/messages.json +4 -0
  53. package/extensions/ublock/_locales/nb/messages.json +4 -0
  54. package/extensions/ublock/_locales/nl/messages.json +4 -0
  55. package/extensions/ublock/_locales/no/messages.json +4 -0
  56. package/extensions/ublock/_locales/oc/messages.json +4 -0
  57. package/extensions/ublock/_locales/pa/messages.json +5 -1
  58. package/extensions/ublock/_locales/pl/messages.json +4 -0
  59. package/extensions/ublock/_locales/pt_BR/messages.json +4 -0
  60. package/extensions/ublock/_locales/pt_PT/messages.json +4 -0
  61. package/extensions/ublock/_locales/ro/messages.json +4 -0
  62. package/extensions/ublock/_locales/ru/messages.json +5 -1
  63. package/extensions/ublock/_locales/si/messages.json +4 -0
  64. package/extensions/ublock/_locales/sk/messages.json +4 -0
  65. package/extensions/ublock/_locales/sl/messages.json +4 -0
  66. package/extensions/ublock/_locales/so/messages.json +4 -0
  67. package/extensions/ublock/_locales/sq/messages.json +4 -0
  68. package/extensions/ublock/_locales/sr/messages.json +4 -0
  69. package/extensions/ublock/_locales/sv/messages.json +5 -1
  70. package/extensions/ublock/_locales/sw/messages.json +4 -0
  71. package/extensions/ublock/_locales/ta/messages.json +4 -0
  72. package/extensions/ublock/_locales/te/messages.json +4 -0
  73. package/extensions/ublock/_locales/th/messages.json +4 -0
  74. package/extensions/ublock/_locales/tr/messages.json +4 -0
  75. package/extensions/ublock/_locales/uk/messages.json +7 -3
  76. package/extensions/ublock/_locales/ur/messages.json +4 -0
  77. package/extensions/ublock/_locales/vi/messages.json +4 -0
  78. package/extensions/ublock/_locales/zh_CN/messages.json +4 -0
  79. package/extensions/ublock/_locales/zh_TW/messages.json +4 -0
  80. package/extensions/ublock/assets/assets.json +14 -14
  81. package/extensions/ublock/assets/resources/run-at.js +80 -0
  82. package/extensions/ublock/assets/resources/safe-self.js +218 -0
  83. package/extensions/ublock/assets/resources/scriptlets.js +461 -538
  84. package/extensions/ublock/assets/resources/set-attr.js +201 -0
  85. package/extensions/ublock/assets/thirdparties/easylist/easylist.txt +4002 -11582
  86. package/extensions/ublock/assets/thirdparties/easylist/easyprivacy.txt +257 -83
  87. package/extensions/ublock/assets/thirdparties/pgl.yoyo.org/as/serverlist +22 -38
  88. package/extensions/ublock/assets/thirdparties/publicsuffix.org/list/effective_tld_names.dat +102 -110
  89. package/extensions/ublock/assets/thirdparties/urlhaus-filter/urlhaus-filter-online.txt +2776 -3613
  90. package/extensions/ublock/assets/ublock/badware.min.txt +581 -382
  91. package/extensions/ublock/assets/ublock/filters.min.txt +997 -990
  92. package/extensions/ublock/assets/ublock/privacy.min.txt +145 -26
  93. package/extensions/ublock/assets/ublock/quick-fixes.min.txt +134 -83
  94. package/extensions/ublock/assets/ublock/unbreak.min.txt +69 -46
  95. package/extensions/ublock/css/document-blocked.css +28 -1
  96. package/extensions/ublock/document-blocked.html +9 -7
  97. package/extensions/ublock/js/assets.js +15 -11
  98. package/extensions/ublock/js/benchmarks.js +13 -9
  99. package/extensions/ublock/js/biditrie.js +1 -1
  100. package/extensions/ublock/js/document-blocked.js +41 -5
  101. package/extensions/ublock/js/hntrie.js +1 -1
  102. package/extensions/ublock/js/i18n.js +2 -4
  103. package/extensions/ublock/js/pagestore.js +61 -43
  104. package/extensions/ublock/js/s14e-serializer.js +47 -22
  105. package/extensions/ublock/js/scriptlet-filtering.js +21 -18
  106. package/extensions/ublock/js/scriptlets/load-large-media-interactive.js +61 -48
  107. package/extensions/ublock/js/scriptlets/should-inject-contentscript.js +1 -3
  108. package/extensions/ublock/js/static-dnr-filtering.js +9 -7
  109. package/extensions/ublock/js/static-filtering-parser.js +87 -54
  110. package/extensions/ublock/js/static-net-filtering.js +118 -42
  111. package/extensions/ublock/js/traffic.js +37 -18
  112. package/extensions/ublock/js/vapi-background-ext.js +21 -49
  113. package/extensions/ublock/js/vapi-background.js +8 -0
  114. package/extensions/ublock/manifest.json +1 -1
  115. package/extensions/ublock/web_accessible_resources/googlesyndication_adsbygoogle.js +3 -1
  116. package/package.json +15 -15
  117. package/static/docs/swagger.json +1 -1
  118. package/static/docs/swagger.min.json +1 -1
@@ -22,6 +22,10 @@
22
22
  web page context.
23
23
  */
24
24
 
25
+ import { setAttr, setAttrFn, trustedSetAttr } from './set-attr.js';
26
+ import { runAt } from './run-at.js';
27
+ import { safeSelf } from './safe-self.js';
28
+
25
29
  /* eslint no-prototype-builtins: 0 */
26
30
 
27
31
  // Externally added to the private namespace in which scriptlets execute.
@@ -29,6 +33,30 @@
29
33
 
30
34
  export const builtinScriptlets = [];
31
35
 
36
+ /******************************************************************************/
37
+
38
+ // Register scriptlets declared in other files.
39
+
40
+ const registerScriptlet = fn => {
41
+ const details = fn.details;
42
+ if ( typeof details !== 'object' ) {
43
+ throw new ReferenceError('Unknown scriptlet function');
44
+ }
45
+ details.fn = fn;
46
+ if ( Array.isArray(details.dependencies) ) {
47
+ details.dependencies.forEach((fn, i, array) => {
48
+ if ( typeof fn !== 'function' ) { return; }
49
+ array[i] = fn.details.name;
50
+ });
51
+ }
52
+ builtinScriptlets.push(details);
53
+ };
54
+
55
+ registerScriptlet(safeSelf);
56
+ registerScriptlet(setAttrFn);
57
+ registerScriptlet(setAttr);
58
+ registerScriptlet(trustedSetAttr);
59
+
32
60
  /*******************************************************************************
33
61
 
34
62
  Helper functions
@@ -37,186 +65,6 @@ export const builtinScriptlets = [];
37
65
 
38
66
  *******************************************************************************/
39
67
 
40
- builtinScriptlets.push({
41
- name: 'safe-self.fn',
42
- fn: safeSelf,
43
- });
44
- function safeSelf() {
45
- if ( scriptletGlobals.safeSelf ) {
46
- return scriptletGlobals.safeSelf;
47
- }
48
- const self = globalThis;
49
- const safe = {
50
- 'Array_from': Array.from,
51
- 'Error': self.Error,
52
- 'Function_toStringFn': self.Function.prototype.toString,
53
- 'Function_toString': thisArg => safe.Function_toStringFn.call(thisArg),
54
- 'Math_floor': Math.floor,
55
- 'Math_max': Math.max,
56
- 'Math_min': Math.min,
57
- 'Math_random': Math.random,
58
- 'Object': Object,
59
- 'Object_defineProperty': Object.defineProperty.bind(Object),
60
- 'Object_defineProperties': Object.defineProperties.bind(Object),
61
- 'Object_fromEntries': Object.fromEntries.bind(Object),
62
- 'Object_getOwnPropertyDescriptor': Object.getOwnPropertyDescriptor.bind(Object),
63
- 'RegExp': self.RegExp,
64
- 'RegExp_test': self.RegExp.prototype.test,
65
- 'RegExp_exec': self.RegExp.prototype.exec,
66
- 'Request_clone': self.Request.prototype.clone,
67
- 'String_fromCharCode': String.fromCharCode,
68
- 'XMLHttpRequest': self.XMLHttpRequest,
69
- 'addEventListener': self.EventTarget.prototype.addEventListener,
70
- 'removeEventListener': self.EventTarget.prototype.removeEventListener,
71
- 'fetch': self.fetch,
72
- 'JSON': self.JSON,
73
- 'JSON_parseFn': self.JSON.parse,
74
- 'JSON_stringifyFn': self.JSON.stringify,
75
- 'JSON_parse': (...args) => safe.JSON_parseFn.call(safe.JSON, ...args),
76
- 'JSON_stringify': (...args) => safe.JSON_stringifyFn.call(safe.JSON, ...args),
77
- 'log': console.log.bind(console),
78
- // Properties
79
- logLevel: 0,
80
- // Methods
81
- makeLogPrefix(...args) {
82
- return this.sendToLogger && `[${args.join(' \u205D ')}]` || '';
83
- },
84
- uboLog(...args) {
85
- if ( this.sendToLogger === undefined ) { return; }
86
- if ( args === undefined || args[0] === '' ) { return; }
87
- return this.sendToLogger('info', ...args);
88
-
89
- },
90
- uboErr(...args) {
91
- if ( this.sendToLogger === undefined ) { return; }
92
- if ( args === undefined || args[0] === '' ) { return; }
93
- return this.sendToLogger('error', ...args);
94
- },
95
- escapeRegexChars(s) {
96
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
97
- },
98
- initPattern(pattern, options = {}) {
99
- if ( pattern === '' ) {
100
- return { matchAll: true, expect: true };
101
- }
102
- const expect = (options.canNegate !== true || pattern.startsWith('!') === false);
103
- if ( expect === false ) {
104
- pattern = pattern.slice(1);
105
- }
106
- const match = /^\/(.+)\/([gimsu]*)$/.exec(pattern);
107
- if ( match !== null ) {
108
- return {
109
- re: new this.RegExp(
110
- match[1],
111
- match[2] || options.flags
112
- ),
113
- expect,
114
- };
115
- }
116
- if ( options.flags !== undefined ) {
117
- return {
118
- re: new this.RegExp(this.escapeRegexChars(pattern),
119
- options.flags
120
- ),
121
- expect,
122
- };
123
- }
124
- return { pattern, expect };
125
- },
126
- testPattern(details, haystack) {
127
- if ( details.matchAll ) { return true; }
128
- if ( details.re ) {
129
- return this.RegExp_test.call(details.re, haystack) === details.expect;
130
- }
131
- return haystack.includes(details.pattern) === details.expect;
132
- },
133
- patternToRegex(pattern, flags = undefined, verbatim = false) {
134
- if ( pattern === '' ) { return /^/; }
135
- const match = /^\/(.+)\/([gimsu]*)$/.exec(pattern);
136
- if ( match === null ) {
137
- const reStr = this.escapeRegexChars(pattern);
138
- return new RegExp(verbatim ? `^${reStr}$` : reStr, flags);
139
- }
140
- try {
141
- return new RegExp(match[1], match[2] || undefined);
142
- }
143
- catch(ex) {
144
- }
145
- return /^/;
146
- },
147
- getExtraArgs(args, offset = 0) {
148
- const entries = args.slice(offset).reduce((out, v, i, a) => {
149
- if ( (i & 1) === 0 ) {
150
- const rawValue = a[i+1];
151
- const value = /^\d+$/.test(rawValue)
152
- ? parseInt(rawValue, 10)
153
- : rawValue;
154
- out.push([ a[i], value ]);
155
- }
156
- return out;
157
- }, []);
158
- return this.Object_fromEntries(entries);
159
- },
160
- onIdle(fn, options) {
161
- if ( self.requestIdleCallback ) {
162
- return self.requestIdleCallback(fn, options);
163
- }
164
- return self.requestAnimationFrame(fn);
165
- },
166
- offIdle(id) {
167
- if ( self.requestIdleCallback ) {
168
- return self.cancelIdleCallback(id);
169
- }
170
- return self.cancelAnimationFrame(id);
171
- }
172
- };
173
- scriptletGlobals.safeSelf = safe;
174
- if ( scriptletGlobals.bcSecret === undefined ) { return safe; }
175
- // This is executed only when the logger is opened
176
- const bc = new self.BroadcastChannel(scriptletGlobals.bcSecret);
177
- let bcBuffer = [];
178
- safe.logLevel = scriptletGlobals.logLevel || 1;
179
- let lastLogType = '';
180
- let lastLogText = '';
181
- let lastLogTime = 0;
182
- safe.sendToLogger = (type, ...args) => {
183
- if ( args.length === 0 ) { return; }
184
- const text = `[${document.location.hostname || document.location.href}]${args.join(' ')}`;
185
- if ( text === lastLogText && type === lastLogType ) {
186
- if ( (Date.now() - lastLogTime) < 5000 ) { return; }
187
- }
188
- lastLogType = type;
189
- lastLogText = text;
190
- lastLogTime = Date.now();
191
- if ( bcBuffer === undefined ) {
192
- return bc.postMessage({ what: 'messageToLogger', type, text });
193
- }
194
- bcBuffer.push({ type, text });
195
- };
196
- bc.onmessage = ev => {
197
- const msg = ev.data;
198
- switch ( msg ) {
199
- case 'iamready!':
200
- if ( bcBuffer === undefined ) { break; }
201
- bcBuffer.forEach(({ type, text }) =>
202
- bc.postMessage({ what: 'messageToLogger', type, text })
203
- );
204
- bcBuffer = undefined;
205
- break;
206
- case 'setScriptletLogLevelToOne':
207
- safe.logLevel = 1;
208
- break;
209
- case 'setScriptletLogLevelToTwo':
210
- safe.logLevel = 2;
211
- break;
212
- }
213
- };
214
- bc.postMessage('areyouready?');
215
- return safe;
216
- }
217
-
218
- /******************************************************************************/
219
-
220
68
  builtinScriptlets.push({
221
69
  name: 'get-random-token.fn',
222
70
  fn: getRandomToken,
@@ -270,34 +118,6 @@ builtinScriptlets.push({
270
118
  'safe-self.fn',
271
119
  ],
272
120
  });
273
- function runAt(fn, when) {
274
- const intFromReadyState = state => {
275
- const targets = {
276
- 'loading': 1, 'asap': 1,
277
- 'interactive': 2, 'end': 2, '2': 2,
278
- 'complete': 3, 'idle': 3, '3': 3,
279
- };
280
- const tokens = Array.isArray(state) ? state : [ state ];
281
- for ( const token of tokens ) {
282
- const prop = `${token}`;
283
- if ( targets.hasOwnProperty(prop) === false ) { continue; }
284
- return targets[prop];
285
- }
286
- return 0;
287
- };
288
- const runAt = intFromReadyState(when);
289
- if ( intFromReadyState(document.readyState) >= runAt ) {
290
- fn(); return;
291
- }
292
- const onStateChange = ( ) => {
293
- if ( intFromReadyState(document.readyState) < runAt ) { return; }
294
- fn();
295
- safe.removeEventListener.apply(document, args);
296
- };
297
- const safe = safeSelf();
298
- const args = [ 'readystatechange', onStateChange, { capture: true } ];
299
- safe.addEventListener.apply(document, args);
300
- }
301
121
 
302
122
  /******************************************************************************/
303
123
 
@@ -321,6 +141,9 @@ function runAtHtmlElementFn(fn) {
321
141
 
322
142
  // Reference:
323
143
  // https://github.com/AdguardTeam/Scriptlets/blob/master/wiki/about-scriptlets.md#prevent-xhr
144
+ //
145
+ // Added `trusted` argument to allow for returning arbitrary text. Can only
146
+ // be used through scriptlets requiring trusted source.
324
147
 
325
148
  builtinScriptlets.push({
326
149
  name: 'generate-content.fn',
@@ -329,7 +152,7 @@ builtinScriptlets.push({
329
152
  'safe-self.fn',
330
153
  ],
331
154
  });
332
- function generateContentFn(directive) {
155
+ function generateContentFn(trusted, directive) {
333
156
  const safe = safeSelf();
334
157
  const randomize = len => {
335
158
  const chunks = [];
@@ -343,27 +166,27 @@ function generateContentFn(directive) {
343
166
  return chunks.join(' ').slice(0, len);
344
167
  };
345
168
  if ( directive === 'true' ) {
346
- return Promise.resolve(randomize(10));
169
+ return randomize(10);
347
170
  }
348
171
  if ( directive === 'emptyObj' ) {
349
- return Promise.resolve('{}');
172
+ return '{}';
350
173
  }
351
174
  if ( directive === 'emptyArr' ) {
352
- return Promise.resolve('[]');
175
+ return '[]';
353
176
  }
354
177
  if ( directive === 'emptyStr' ) {
355
- return Promise.resolve('');
178
+ return '';
356
179
  }
357
180
  if ( directive.startsWith('length:') ) {
358
181
  const match = /^length:(\d+)(?:-(\d+))?$/.exec(directive);
359
- if ( match ) {
360
- const min = parseInt(match[1], 10);
361
- const extent = safe.Math_max(parseInt(match[2], 10) || 0, min) - min;
362
- const len = safe.Math_min(min + extent * safe.Math_random(), 500000);
363
- return Promise.resolve(randomize(len | 0));
364
- }
182
+ if ( match === null ) { return ''; }
183
+ const min = parseInt(match[1], 10);
184
+ const extent = safe.Math_max(parseInt(match[2], 10) || 0, min) - min;
185
+ const len = safe.Math_min(min + extent * safe.Math_random(), 500000);
186
+ return randomize(len | 0);
365
187
  }
366
- if ( directive.startsWith('war:') && scriptletGlobals.warOrigin ) {
188
+ if ( directive.startsWith('war:') ) {
189
+ if ( scriptletGlobals.warOrigin === undefined ) { return ''; }
367
190
  return new Promise(resolve => {
368
191
  const warOrigin = scriptletGlobals.warOrigin;
369
192
  const warName = directive.slice(4);
@@ -379,9 +202,12 @@ function generateContentFn(directive) {
379
202
  };
380
203
  warXHR.open('GET', fullpath.join(''));
381
204
  warXHR.send();
382
- });
205
+ }).catch(( ) => '');
383
206
  }
384
- return Promise.resolve('');
207
+ if ( trusted ) {
208
+ return directive;
209
+ }
210
+ return '';
385
211
  }
386
212
 
387
213
  /******************************************************************************/
@@ -983,6 +809,7 @@ function getSafeCookieValuesFn() {
983
809
  'enable', 'disable',
984
810
  'enabled', 'disabled',
985
811
  'essential', 'nonessential',
812
+ 'forbidden', 'forever',
986
813
  'hide', 'hidden',
987
814
  'necessary', 'required',
988
815
  'ok',
@@ -1565,6 +1392,196 @@ function proxyApplyFn(
1565
1392
  context[prop] = new Proxy(fn, proxyDetails);
1566
1393
  }
1567
1394
 
1395
+ /******************************************************************************/
1396
+
1397
+ builtinScriptlets.push({
1398
+ name: 'prevent-xhr.fn',
1399
+ fn: preventXhrFn,
1400
+ dependencies: [
1401
+ 'generate-content.fn',
1402
+ 'match-object-properties.fn',
1403
+ 'parse-properties-to-match.fn',
1404
+ 'safe-self.fn',
1405
+ ],
1406
+ });
1407
+ function preventXhrFn(
1408
+ trusted = false,
1409
+ propsToMatch = '',
1410
+ directive = ''
1411
+ ) {
1412
+ if ( typeof propsToMatch !== 'string' ) { return; }
1413
+ const safe = safeSelf();
1414
+ const scriptletName = trusted ? 'trusted-prevent-xhr' : 'prevent-xhr';
1415
+ const logPrefix = safe.makeLogPrefix(scriptletName, propsToMatch, directive);
1416
+ const xhrInstances = new WeakMap();
1417
+ const propNeedles = parsePropertiesToMatch(propsToMatch, 'url');
1418
+ const warOrigin = scriptletGlobals.warOrigin;
1419
+ const safeDispatchEvent = (xhr, type) => {
1420
+ try {
1421
+ xhr.dispatchEvent(new Event(type));
1422
+ } catch(_) {
1423
+ }
1424
+ };
1425
+ const XHRBefore = XMLHttpRequest.prototype;
1426
+ self.XMLHttpRequest = class extends self.XMLHttpRequest {
1427
+ open(method, url, ...args) {
1428
+ xhrInstances.delete(this);
1429
+ if ( warOrigin !== undefined && url.startsWith(warOrigin) ) {
1430
+ return super.open(method, url, ...args);
1431
+ }
1432
+ const haystack = { method, url };
1433
+ if ( propsToMatch === '' && directive === '' ) {
1434
+ safe.uboLog(logPrefix, `Called: ${safe.JSON_stringify(haystack, null, 2)}`);
1435
+ return super.open(method, url, ...args);
1436
+ }
1437
+ if ( matchObjectProperties(propNeedles, haystack) ) {
1438
+ const xhrDetails = Object.assign(haystack, {
1439
+ xhr: this,
1440
+ defer: args.length === 0 || !!args[0],
1441
+ directive,
1442
+ headers: {
1443
+ 'date': '',
1444
+ 'content-type': '',
1445
+ 'content-length': '',
1446
+ },
1447
+ props: {
1448
+ response: { value: '' },
1449
+ responseText: { value: '' },
1450
+ responseXML: { value: null },
1451
+ responseURL: { value: haystack.url },
1452
+ },
1453
+ });
1454
+ xhrInstances.set(this, xhrDetails);
1455
+ }
1456
+ return super.open(method, url, ...args);
1457
+ }
1458
+ send(...args) {
1459
+ const xhrDetails = xhrInstances.get(this);
1460
+ if ( xhrDetails === undefined ) {
1461
+ return super.send(...args);
1462
+ }
1463
+ xhrDetails.headers['date'] = (new Date()).toUTCString();
1464
+ let xhrText = '';
1465
+ switch ( this.responseType ) {
1466
+ case 'arraybuffer':
1467
+ xhrDetails.props.response.value = new ArrayBuffer(0);
1468
+ xhrDetails.headers['content-type'] = 'application/octet-stream';
1469
+ break;
1470
+ case 'blob':
1471
+ xhrDetails.props.response.value = new Blob([]);
1472
+ xhrDetails.headers['content-type'] = 'application/octet-stream';
1473
+ break;
1474
+ case 'document': {
1475
+ const parser = new DOMParser();
1476
+ const doc = parser.parseFromString('', 'text/html');
1477
+ xhrDetails.props.response.value = doc;
1478
+ xhrDetails.props.responseXML.value = doc;
1479
+ xhrDetails.headers['content-type'] = 'text/html';
1480
+ break;
1481
+ }
1482
+ case 'json':
1483
+ xhrDetails.props.response.value = {};
1484
+ xhrDetails.props.responseText.value = '{}';
1485
+ xhrDetails.headers['content-type'] = 'application/json';
1486
+ break;
1487
+ default: {
1488
+ if ( directive === '' ) { break; }
1489
+ xhrText = generateContentFn(trusted, xhrDetails.directive);
1490
+ if ( xhrText instanceof Promise ) {
1491
+ xhrText = xhrText.then(text => {
1492
+ xhrDetails.props.response.value = text;
1493
+ xhrDetails.props.responseText.value = text;
1494
+ });
1495
+ } else {
1496
+ xhrDetails.props.response.value = xhrText;
1497
+ xhrDetails.props.responseText.value = xhrText;
1498
+ }
1499
+ xhrDetails.headers['content-type'] = 'text/plain';
1500
+ break;
1501
+ }
1502
+ }
1503
+ if ( xhrDetails.defer === false ) {
1504
+ xhrDetails.headers['content-length'] = `${xhrDetails.props.response.value}`.length;
1505
+ Object.defineProperties(xhrDetails.xhr, {
1506
+ readyState: { value: 4 },
1507
+ status: { value: 200 },
1508
+ statusText: { value: 'OK' },
1509
+ });
1510
+ Object.defineProperties(xhrDetails.xhr, xhrDetails.props);
1511
+ return;
1512
+ }
1513
+ Promise.resolve(xhrText).then(( ) => xhrDetails).then(details => {
1514
+ Object.defineProperties(details.xhr, {
1515
+ readyState: { value: 1, configurable: true },
1516
+ });
1517
+ safeDispatchEvent(details.xhr, 'readystatechange');
1518
+ return details;
1519
+ }).then(details => {
1520
+ xhrDetails.headers['content-length'] = `${details.props.response.value}`.length;
1521
+ Object.defineProperties(details.xhr, {
1522
+ readyState: { value: 2, configurable: true },
1523
+ status: { value: 200 },
1524
+ statusText: { value: 'OK' },
1525
+ });
1526
+ safeDispatchEvent(details.xhr, 'readystatechange');
1527
+ return details;
1528
+ }).then(details => {
1529
+ Object.defineProperties(details.xhr, {
1530
+ readyState: { value: 3, configurable: true },
1531
+ });
1532
+ Object.defineProperties(details.xhr, details.props);
1533
+ safeDispatchEvent(details.xhr, 'readystatechange');
1534
+ return details;
1535
+ }).then(details => {
1536
+ Object.defineProperties(details.xhr, {
1537
+ readyState: { value: 4 },
1538
+ });
1539
+ safeDispatchEvent(details.xhr, 'readystatechange');
1540
+ safeDispatchEvent(details.xhr, 'load');
1541
+ safeDispatchEvent(details.xhr, 'loadend');
1542
+ safe.uboLog(logPrefix, `Prevented with response:\n${details.xhr.response}`);
1543
+ });
1544
+ }
1545
+ getResponseHeader(headerName) {
1546
+ const xhrDetails = xhrInstances.get(this);
1547
+ if ( xhrDetails === undefined || this.readyState < this.HEADERS_RECEIVED ) {
1548
+ return super.getResponseHeader(headerName);
1549
+ }
1550
+ const value = xhrDetails.headers[headerName.toLowerCase()];
1551
+ if ( value !== undefined && value !== '' ) { return value; }
1552
+ return null;
1553
+ }
1554
+ getAllResponseHeaders() {
1555
+ const xhrDetails = xhrInstances.get(this);
1556
+ if ( xhrDetails === undefined || this.readyState < this.HEADERS_RECEIVED ) {
1557
+ return super.getAllResponseHeaders();
1558
+ }
1559
+ const out = [];
1560
+ for ( const [ name, value ] of Object.entries(xhrDetails.headers) ) {
1561
+ if ( !value ) { continue; }
1562
+ out.push(`${name}: ${value}`);
1563
+ }
1564
+ if ( out.length !== 0 ) { out.push(''); }
1565
+ return out.join('\r\n');
1566
+ }
1567
+ };
1568
+ self.XMLHttpRequest.prototype.open.toString = function() {
1569
+ return XHRBefore.open.toString();
1570
+ };
1571
+ self.XMLHttpRequest.prototype.send.toString = function() {
1572
+ return XHRBefore.send.toString();
1573
+ };
1574
+ self.XMLHttpRequest.prototype.getResponseHeader.toString = function() {
1575
+ return XHRBefore.getResponseHeader.toString();
1576
+ };
1577
+ self.XMLHttpRequest.prototype.getAllResponseHeaders.toString = function() {
1578
+ return XHRBefore.getAllResponseHeaders.toString();
1579
+ };
1580
+ }
1581
+
1582
+
1583
+
1584
+
1568
1585
  /*******************************************************************************
1569
1586
 
1570
1587
  Injectable scriptlets
@@ -2271,7 +2288,7 @@ function noFetchIf(
2271
2288
  if ( proceed ) {
2272
2289
  return context.reflect();
2273
2290
  }
2274
- return generateContentFn(responseBody).then(text => {
2291
+ return Promise.resolve(generateContentFn(false, responseBody)).then(text => {
2275
2292
  safe.uboLog(logPrefix, `Prevented with response "${text}"`);
2276
2293
  const response = new Response(text, {
2277
2294
  headers: {
@@ -2298,30 +2315,34 @@ builtinScriptlets.push({
2298
2315
  fn: preventRefresh,
2299
2316
  world: 'ISOLATED',
2300
2317
  dependencies: [
2301
- 'run-at.fn',
2302
2318
  'safe-self.fn',
2303
2319
  ],
2304
2320
  });
2305
2321
  // https://www.reddit.com/r/uBlockOrigin/comments/q0frv0/while_reading_a_sports_article_i_was_redirected/hf7wo9v/
2306
2322
  function preventRefresh(
2307
- arg1 = ''
2323
+ delay = ''
2308
2324
  ) {
2309
- if ( typeof arg1 !== 'string' ) { return; }
2325
+ if ( typeof delay !== 'string' ) { return; }
2310
2326
  const safe = safeSelf();
2311
- const logPrefix = safe.makeLogPrefix('prevent-refresh', arg1);
2327
+ const logPrefix = safe.makeLogPrefix('prevent-refresh', delay);
2328
+ const stop = content => {
2329
+ window.stop();
2330
+ safe.uboLog(logPrefix, `Prevented "${content}"`);
2331
+ };
2312
2332
  const defuse = ( ) => {
2313
2333
  const meta = document.querySelector('meta[http-equiv="refresh" i][content]');
2314
2334
  if ( meta === null ) { return; }
2315
- safe.uboLog(logPrefix, `Prevented "${meta.textContent}"`);
2316
- const s = arg1 === ''
2317
- ? meta.getAttribute('content')
2318
- : arg1;
2319
- const ms = Math.max(parseFloat(s) || 0, 0) * 1000;
2320
- setTimeout(( ) => { window.stop(); }, ms);
2335
+ const content = meta.getAttribute('content') || '';
2336
+ const ms = delay === ''
2337
+ ? Math.max(parseFloat(content) || 0, 0) * 500
2338
+ : 0;
2339
+ if ( ms === 0 ) {
2340
+ stop(content);
2341
+ } else {
2342
+ setTimeout(( ) => { stop(content); }, ms);
2343
+ }
2321
2344
  };
2322
- runAt(( ) => {
2323
- defuse();
2324
- }, 'interactive');
2345
+ self.addEventListener('load', defuse, { capture: true, once: true });
2325
2346
  }
2326
2347
 
2327
2348
  /******************************************************************************/
@@ -2730,201 +2751,58 @@ function webrtcIf(
2730
2751
  /******************************************************************************/
2731
2752
 
2732
2753
  builtinScriptlets.push({
2733
- name: 'no-xhr-if.js',
2754
+ name: 'prevent-xhr.js',
2734
2755
  aliases: [
2735
- 'prevent-xhr.js',
2756
+ 'no-xhr-if.js',
2736
2757
  ],
2737
- fn: noXhrIf,
2758
+ fn: preventXhr,
2738
2759
  dependencies: [
2739
- 'generate-content.fn',
2740
- 'match-object-properties.fn',
2741
- 'parse-properties-to-match.fn',
2742
- 'safe-self.fn',
2760
+ 'prevent-xhr.fn',
2743
2761
  ],
2744
2762
  });
2745
- function noXhrIf(
2746
- propsToMatch = '',
2747
- directive = ''
2748
- ) {
2749
- if ( typeof propsToMatch !== 'string' ) { return; }
2750
- const safe = safeSelf();
2751
- const logPrefix = safe.makeLogPrefix('prevent-xhr', propsToMatch, directive);
2752
- const xhrInstances = new WeakMap();
2753
- const propNeedles = parsePropertiesToMatch(propsToMatch, 'url');
2754
- const warOrigin = scriptletGlobals.warOrigin;
2755
- const headers = {
2756
- 'date': '',
2757
- 'content-type': '',
2758
- 'content-length': '',
2759
- };
2760
- const safeDispatchEvent = (xhr, type) => {
2761
- try {
2762
- xhr.dispatchEvent(new Event(type));
2763
- } catch(_) {
2764
- }
2765
- };
2766
- const XHRBefore = XMLHttpRequest.prototype;
2767
- self.XMLHttpRequest = class extends self.XMLHttpRequest {
2768
- open(method, url, ...args) {
2769
- xhrInstances.delete(this);
2770
- if ( warOrigin !== undefined && url.startsWith(warOrigin) ) {
2771
- return super.open(method, url, ...args);
2772
- }
2773
- const haystack = { method, url };
2774
- if ( propsToMatch === '' && directive === '' ) {
2775
- safe.uboLog(logPrefix, `Called: ${safe.JSON_stringify(haystack, null, 2)}`);
2776
- return super.open(method, url, ...args);
2777
- }
2778
- if ( matchObjectProperties(propNeedles, haystack) ) {
2779
- xhrInstances.set(this, haystack);
2780
- }
2781
- haystack.headers = Object.assign({}, headers);
2782
- return super.open(method, url, ...args);
2783
- }
2784
- send(...args) {
2785
- const haystack = xhrInstances.get(this);
2786
- if ( haystack === undefined ) {
2787
- return super.send(...args);
2788
- }
2789
- haystack.headers['date'] = (new Date()).toUTCString();
2790
- let promise = Promise.resolve({
2791
- xhr: this,
2792
- directive,
2793
- response: {
2794
- response: { value: '' },
2795
- responseText: { value: '' },
2796
- responseXML: { value: null },
2797
- responseURL: { value: haystack.url },
2798
- }
2799
- });
2800
- switch ( this.responseType ) {
2801
- case 'arraybuffer':
2802
- promise = promise.then(details => {
2803
- const response = details.response;
2804
- response.response.value = new ArrayBuffer(0);
2805
- return details;
2806
- });
2807
- haystack.headers['content-type'] = 'application/octet-stream';
2808
- break;
2809
- case 'blob':
2810
- promise = promise.then(details => {
2811
- const response = details.response;
2812
- response.response.value = new Blob([]);
2813
- return details;
2814
- });
2815
- haystack.headers['content-type'] = 'application/octet-stream';
2816
- break;
2817
- case 'document': {
2818
- promise = promise.then(details => {
2819
- const parser = new DOMParser();
2820
- const doc = parser.parseFromString('', 'text/html');
2821
- const response = details.response;
2822
- response.response.value = doc;
2823
- response.responseXML.value = doc;
2824
- return details;
2825
- });
2826
- haystack.headers['content-type'] = 'text/html';
2827
- break;
2828
- }
2829
- case 'json':
2830
- promise = promise.then(details => {
2831
- const response = details.response;
2832
- response.response.value = {};
2833
- response.responseText.value = '{}';
2834
- return details;
2835
- });
2836
- haystack.headers['content-type'] = 'application/json';
2837
- break;
2838
- default:
2839
- if ( directive === '' ) { break; }
2840
- promise = promise.then(details => {
2841
- return generateContentFn(details.directive).then(text => {
2842
- const response = details.response;
2843
- response.response.value = text;
2844
- response.responseText.value = text;
2845
- return details;
2846
- });
2847
- });
2848
- haystack.headers['content-type'] = 'text/plain';
2849
- break;
2850
- }
2851
- promise.then(details => {
2852
- Object.defineProperties(details.xhr, {
2853
- readyState: { value: 1, configurable: true },
2854
- });
2855
- safeDispatchEvent(details.xhr, 'readystatechange');
2856
- return details;
2857
- }).then(details => {
2858
- const response = details.response;
2859
- haystack.headers['content-length'] = `${response.response.value}`.length;
2860
- Object.defineProperties(details.xhr, {
2861
- readyState: { value: 2, configurable: true },
2862
- status: { value: 200 },
2863
- statusText: { value: 'OK' },
2864
- });
2865
- safeDispatchEvent(details.xhr, 'readystatechange');
2866
- return details;
2867
- }).then(details => {
2868
- Object.defineProperties(details.xhr, {
2869
- readyState: { value: 3, configurable: true },
2870
- });
2871
- Object.defineProperties(details.xhr, details.response);
2872
- safeDispatchEvent(details.xhr, 'readystatechange');
2873
- return details;
2874
- }).then(details => {
2875
- Object.defineProperties(details.xhr, {
2876
- readyState: { value: 4 },
2877
- });
2878
- safeDispatchEvent(details.xhr, 'readystatechange');
2879
- safeDispatchEvent(details.xhr, 'load');
2880
- safeDispatchEvent(details.xhr, 'loadend');
2881
- safe.uboLog(logPrefix, `Prevented with response:\n${details.xhr.response}`);
2882
- });
2883
- }
2884
- getResponseHeader(headerName) {
2885
- const haystack = xhrInstances.get(this);
2886
- if ( haystack === undefined || this.readyState < this.HEADERS_RECEIVED ) {
2887
- return super.getResponseHeader(headerName);
2888
- }
2889
- const value = haystack.headers[headerName.toLowerCase()];
2890
- if ( value !== undefined && value !== '' ) { return value; }
2891
- return null;
2892
- }
2893
- getAllResponseHeaders() {
2894
- const haystack = xhrInstances.get(this);
2895
- if ( haystack === undefined || this.readyState < this.HEADERS_RECEIVED ) {
2896
- return super.getAllResponseHeaders();
2897
- }
2898
- const out = [];
2899
- for ( const [ name, value ] of Object.entries(haystack.headers) ) {
2900
- if ( !value ) { continue; }
2901
- out.push(`${name}: ${value}`);
2902
- }
2903
- if ( out.length !== 0 ) { out.push(''); }
2904
- return out.join('\r\n');
2905
- }
2906
- };
2907
- self.XMLHttpRequest.prototype.open.toString = function() {
2908
- return XHRBefore.open.toString();
2909
- };
2910
- self.XMLHttpRequest.prototype.send.toString = function() {
2911
- return XHRBefore.send.toString();
2912
- };
2913
- self.XMLHttpRequest.prototype.getResponseHeader.toString = function() {
2914
- return XHRBefore.getResponseHeader.toString();
2915
- };
2916
- self.XMLHttpRequest.prototype.getAllResponseHeaders.toString = function() {
2917
- return XHRBefore.getAllResponseHeaders.toString();
2918
- };
2763
+ function preventXhr(...args) {
2764
+ return preventXhrFn(false, ...args);
2919
2765
  }
2920
2766
 
2921
- /******************************************************************************/
2767
+ /**
2768
+ * @scriptlet prevent-window-open
2769
+ *
2770
+ * @description
2771
+ * Prevent a webpage from opening new tabs through `window.open()`.
2772
+ *
2773
+ * @param pattern
2774
+ * A plain string or regex to match against the `url` argument for the
2775
+ * prevention to be triggered. If not provided, all calls to `window.open()`
2776
+ * are prevented.
2777
+ * If set to the special value `debug` *and* the logger is opened, the scriptlet
2778
+ * will trigger a `debugger` statement and the prevention will not occur.
2779
+ *
2780
+ * @param [delay]
2781
+ * If provided, a decoy will be created or opened, and this parameter states
2782
+ * the number of seconds to wait for before the decoy is terminated, i.e.
2783
+ * either removed from the DOM or closed.
2784
+ *
2785
+ * @param [decoy]
2786
+ * A string representing the type of decoy to use:
2787
+ * - `blank`: replace the `url` parameter with `about:blank`
2788
+ * - `object`: create and append an `object` element to the DOM, and return
2789
+ * its `contentWindow` property.
2790
+ * - `frame`: create and append an `iframe` element to the DOM, and return
2791
+ * its `contentWindow` property.
2792
+ *
2793
+ * @example
2794
+ * ##+js(prevent-window-open, ads.example.com/)
2795
+ *
2796
+ * @example
2797
+ * ##+js(prevent-window-open, ads.example.com/, 1, iframe)
2798
+ *
2799
+ * */
2922
2800
 
2923
2801
  builtinScriptlets.push({
2924
- name: 'no-window-open-if.js',
2802
+ name: 'prevent-window-open.js',
2925
2803
  aliases: [
2926
2804
  'nowoif.js',
2927
- 'prevent-window-open.js',
2805
+ 'no-window-open-if.js',
2928
2806
  'window.open-defuser.js',
2929
2807
  ],
2930
2808
  fn: noWindowOpenIf,
@@ -2960,6 +2838,10 @@ function noWindowOpenIf(
2960
2838
  };
2961
2839
  const noopFunc = function(){};
2962
2840
  proxyApplyFn('open', function open(context) {
2841
+ if ( pattern === 'debug' && safe.logLevel !== 0 ) {
2842
+ debugger; // eslint-disable-line no-debugger
2843
+ return context.reflect();
2844
+ }
2963
2845
  const { callArgs } = context;
2964
2846
  const haystack = callArgs.join(' ');
2965
2847
  if ( rePattern.test(haystack) !== targetMatchResult ) {
@@ -4048,124 +3930,6 @@ function setSessionStorageItem(key = '', value = '') {
4048
3930
  setLocalStorageItemFn('session', false, key, value);
4049
3931
  }
4050
3932
 
4051
- /*******************************************************************************
4052
- *
4053
- * @scriptlet set-attr
4054
- *
4055
- * @description
4056
- * Sets the specified attribute on the specified elements. This scriptlet runs
4057
- * once when the page loads then afterward on DOM mutations.
4058
-
4059
- * Reference: https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-attr.js
4060
- *
4061
- * ### Syntax
4062
- *
4063
- * ```text
4064
- * example.org##+js(set-attr, selector, attr [, value])
4065
- * ```
4066
- *
4067
- * - `selector`: CSS selector of DOM elements for which the attribute `attr`
4068
- * must be modified.
4069
- * - `attr`: the name of the attribute to modify
4070
- * - `value`: the value to assign to the target attribute. Possible values:
4071
- * - `''`: empty string (default)
4072
- * - `true`
4073
- * - `false`
4074
- * - positive decimal integer 0 <= value < 32768
4075
- * - `[other]`: copy the value from attribute `other` on the same element
4076
- * */
4077
-
4078
- builtinScriptlets.push({
4079
- name: 'set-attr.js',
4080
- fn: setAttr,
4081
- world: 'ISOLATED',
4082
- dependencies: [
4083
- 'run-at.fn',
4084
- 'safe-self.fn',
4085
- ],
4086
- });
4087
- function setAttr(
4088
- selector = '',
4089
- attr = '',
4090
- value = ''
4091
- ) {
4092
- if ( selector === '' ) { return; }
4093
- if ( attr === '' ) { return; }
4094
-
4095
- const safe = safeSelf();
4096
- const logPrefix = safe.makeLogPrefix('set-attr', attr, value);
4097
- const validValues = [ '', 'false', 'true' ];
4098
- let copyFrom = '';
4099
-
4100
- if ( validValues.includes(value.toLowerCase()) === false ) {
4101
- if ( /^\d+$/.test(value) ) {
4102
- const n = parseInt(value, 10);
4103
- if ( n >= 32768 ) { return; }
4104
- value = `${n}`;
4105
- } else if ( /^\[.+\]$/.test(value) ) {
4106
- copyFrom = value.slice(1, -1);
4107
- } else {
4108
- return;
4109
- }
4110
- }
4111
-
4112
- const extractValue = elem => {
4113
- if ( copyFrom !== '' ) {
4114
- return elem.getAttribute(copyFrom) || '';
4115
- }
4116
- return value;
4117
- };
4118
-
4119
- const applySetAttr = ( ) => {
4120
- const elems = [];
4121
- try {
4122
- elems.push(...document.querySelectorAll(selector));
4123
- }
4124
- catch(ex) {
4125
- return false;
4126
- }
4127
- for ( const elem of elems ) {
4128
- const before = elem.getAttribute(attr);
4129
- const after = extractValue(elem);
4130
- if ( after === before ) { continue; }
4131
- if ( after !== '' && /^on/i.test(attr) ) {
4132
- if ( attr.toLowerCase() in elem ) { continue; }
4133
- }
4134
- elem.setAttribute(attr, after);
4135
- safe.uboLog(logPrefix, `${attr}="${after}"`);
4136
- }
4137
- return true;
4138
- };
4139
- let observer, timer;
4140
- const onDomChanged = mutations => {
4141
- if ( timer !== undefined ) { return; }
4142
- let shouldWork = false;
4143
- for ( const mutation of mutations ) {
4144
- if ( mutation.addedNodes.length === 0 ) { continue; }
4145
- for ( const node of mutation.addedNodes ) {
4146
- if ( node.nodeType !== 1 ) { continue; }
4147
- shouldWork = true;
4148
- break;
4149
- }
4150
- if ( shouldWork ) { break; }
4151
- }
4152
- if ( shouldWork === false ) { return; }
4153
- timer = self.requestAnimationFrame(( ) => {
4154
- timer = undefined;
4155
- applySetAttr();
4156
- });
4157
- };
4158
- const start = ( ) => {
4159
- if ( applySetAttr() === false ) { return; }
4160
- observer = new MutationObserver(onDomChanged);
4161
- observer.observe(document.body, {
4162
- subtree: true,
4163
- childList: true,
4164
- });
4165
- };
4166
- runAt(( ) => { start(); }, 'idle');
4167
- }
4168
-
4169
3933
  /*******************************************************************************
4170
3934
  *
4171
3935
  * @scriptlet prevent-canvas
@@ -5103,4 +4867,163 @@ function trustedSuppressNativeMethod(
5103
4867
  });
5104
4868
  }
5105
4869
 
4870
+ /*******************************************************************************
4871
+ *
4872
+ * Trusted version of prevent-xhr(), which allows the use of an arbitrary
4873
+ * string as response text.
4874
+ *
4875
+ * */
4876
+
4877
+ builtinScriptlets.push({
4878
+ name: 'trusted-prevent-xhr.js',
4879
+ requiresTrust: true,
4880
+ fn: trustedPreventXhr,
4881
+ dependencies: [
4882
+ 'prevent-xhr.fn',
4883
+ ],
4884
+ });
4885
+ function trustedPreventXhr(...args) {
4886
+ return preventXhrFn(true, ...args);
4887
+ }
4888
+
4889
+ /**
4890
+ * @trustedScriptlet trusted-prevent-dom-bypass
4891
+ *
4892
+ * @description
4893
+ * Prevent the bypassing of uBO scriptlets through anonymous embedded context.
4894
+ *
4895
+ * Ensure that a target method in the embedded context is using the
4896
+ * corresponding parent context's method (which is assumed to be
4897
+ * properly patched), or to replace the embedded context with that of the
4898
+ * parent context.
4899
+ *
4900
+ * Root issue:
4901
+ * https://issues.chromium.org/issues/40202434
4902
+ *
4903
+ * @param methodPath
4904
+ * The method which calls must be intercepted. The arguments
4905
+ * of the intercepted calls are assumed to be HTMLElement, anything else will
4906
+ * be ignored.
4907
+ *
4908
+ * @param [targetProp]
4909
+ * The method in the embedded context which should be delegated to the
4910
+ * parent context. If no method is specified, the embedded context becomes
4911
+ * the parent one, i.e. all properties of the embedded context will be that
4912
+ * of the parent context.
4913
+ *
4914
+ * @example
4915
+ * ##+js(trusted-prevent-dom-bypass, Element.prototype.append, open)
4916
+ *
4917
+ * @example
4918
+ * ##+js(trusted-prevent-dom-bypass, Element.prototype.appendChild, XMLHttpRequest)
4919
+ *
4920
+ * */
4921
+
4922
+ builtinScriptlets.push({
4923
+ name: 'trusted-prevent-dom-bypass.js',
4924
+ requiresTrust: true,
4925
+ fn: trustedPreventDomBypass,
4926
+ dependencies: [
4927
+ 'proxy-apply.fn',
4928
+ 'safe-self.fn',
4929
+ ],
4930
+ });
4931
+ function trustedPreventDomBypass(
4932
+ methodPath = '',
4933
+ targetProp = ''
4934
+ ) {
4935
+ if ( methodPath === '' ) { return; }
4936
+ const safe = safeSelf();
4937
+ const logPrefix = safe.makeLogPrefix('trusted-prevent-dom-bypass', methodPath, targetProp);
4938
+ proxyApplyFn(methodPath, function(context) {
4939
+ const elems = new Set(context.callArgs.filter(e => e instanceof HTMLElement));
4940
+ const r = context.reflect();
4941
+ if ( elems.length === 0 ) { return r; }
4942
+ for ( const elem of elems ) {
4943
+ try {
4944
+ if ( `${elem.contentWindow}` !== '[object Window]' ) { continue; }
4945
+ if ( elem.contentWindow.location.href !== 'about:blank' ) {
4946
+ if ( elem.contentWindow.location.href !== self.location.href ) {
4947
+ continue;
4948
+ }
4949
+ }
4950
+ if ( targetProp !== '' ) {
4951
+ elem.contentWindow[targetProp] = self[targetProp];
4952
+ } else {
4953
+ Object.defineProperty(elem, 'contentWindow', { value: self });
4954
+ }
4955
+ safe.uboLog(logPrefix, 'Bypass prevented');
4956
+ } catch(_) {
4957
+ }
4958
+ }
4959
+ return r;
4960
+ });
4961
+ }
4962
+
4963
+ /**
4964
+ * @trustedScriptlet trusted-override-element-method
4965
+ *
4966
+ * @description
4967
+ * Override the behavior of a method on matching elements.
4968
+ *
4969
+ * @param methodPath
4970
+ * The method which calls must be intercepted.
4971
+ *
4972
+ * @param [selector]
4973
+ * A CSS selector which the target element must match. If not specified,
4974
+ * the override will occur for all elements.
4975
+ *
4976
+ * @param [disposition]
4977
+ * How the override should be handled. If not specified, the overridden call
4978
+ * will be equivalent to an empty function. If set to `throw`, an exception
4979
+ * will be thrown. Any other value will be validated and returned as a
4980
+ * supported safe constant.
4981
+ *
4982
+ * @example
4983
+ * ##+js(trusted-override-element-method, HTMLAnchorElement.prototype.click, a[target="_blank"][style])
4984
+ *
4985
+ * */
4986
+
4987
+ builtinScriptlets.push({
4988
+ name: 'trusted-override-element-method.js',
4989
+ requiresTrust: true,
4990
+ fn: trustedOverrideElementMethod,
4991
+ dependencies: [
4992
+ 'proxy-apply.fn',
4993
+ 'safe-self.fn',
4994
+ 'validate-constant.fn',
4995
+ ],
4996
+ });
4997
+ function trustedOverrideElementMethod(
4998
+ methodPath = '',
4999
+ selector = '',
5000
+ disposition = ''
5001
+ ) {
5002
+ if ( methodPath === '' ) { return; }
5003
+ const safe = safeSelf();
5004
+ const logPrefix = safe.makeLogPrefix('trusted-override-element-method', methodPath, selector, disposition);
5005
+ proxyApplyFn(methodPath, function(context) {
5006
+ let override = selector === '';
5007
+ if ( override === false ) {
5008
+ const { thisArg } = context;
5009
+ try {
5010
+ override = thisArg.closest(selector) === thisArg;
5011
+ } catch(_) {
5012
+ }
5013
+ }
5014
+ if ( override === false ) {
5015
+ return context.reflect();
5016
+ }
5017
+ safe.uboLog(logPrefix, 'Overridden');
5018
+ if ( disposition === '' ) { return; }
5019
+ if ( disposition === 'debug' && safe.logLevel !== 0 ) {
5020
+ debugger; // eslint-disable-line no-debugger
5021
+ }
5022
+ if ( disposition === 'throw' ) {
5023
+ throw new ReferenceError();
5024
+ }
5025
+ return validateConstantFn(false, disposition);
5026
+ });
5027
+ }
5028
+
5106
5029
  /******************************************************************************/