contentscript.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. /*******************************************************************************
  2. ηMatrix - a browser extension to black/white list requests.
  3. Copyright (C) 2014-2019 Raymond Hill
  4. Copyright (C) 2019-2022 Alessio Vanni
  5. This program is free software: you can redistribute it and/or modify
  6. it under the terms of the GNU General Public License as published by
  7. the Free Software Foundation, either version 3 of the License, or
  8. (at your option) any later version.
  9. This program is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. GNU General Public License for more details.
  13. You should have received a copy of the GNU General Public License
  14. along with this program. If not, see {http://www.gnu.org/licenses/}.
  15. Home: https://gitlab.com/vannilla/ematrix
  16. uMatrix Home: https://github.com/gorhill/uMatrix
  17. */
  18. 'use strict';
  19. // Injected into content pages
  20. (function () {
  21. // https://github.com/chrisaljoudi/uBlock/issues/464
  22. // https://github.com/gorhill/uMatrix/issues/621
  23. if (document instanceof HTMLDocument === false
  24. && document instanceof XMLDocument === false) {
  25. return;
  26. }
  27. // This can also happen (for example if script injected into a
  28. // `data:` URI doc)
  29. if (!window.location) {
  30. return;
  31. }
  32. // This can happen
  33. if (typeof vAPI !== 'object') {
  34. //console.debug('contentscript.js > vAPI not found');
  35. return;
  36. }
  37. // https://github.com/chrisaljoudi/uBlock/issues/456
  38. // Already injected?
  39. if (vAPI.contentscriptEndInjected) {
  40. //console.debug('contentscript.js > content script already injected');
  41. return;
  42. }
  43. vAPI.contentscriptEndInjected = true;
  44. // Executed only once.
  45. (function () {
  46. let localStorageHandler = function (mustRemove) {
  47. if (mustRemove) {
  48. window.localStorage.clear();
  49. window.sessionStorage.clear();
  50. }
  51. };
  52. // Check with extension whether local storage must be emptied
  53. // rhill 2014-03-28: we need an exception handler in case
  54. // 3rd-party access to site data is disabled.
  55. // https://github.com/gorhill/httpswitchboard/issues/215
  56. try {
  57. let hasLocalStorage =
  58. window.localStorage && window.localStorage.length !== 0;
  59. let hasSessionStorage =
  60. window.sessionStorage && window.sessionStorage.length !== 0;
  61. if (hasLocalStorage || hasSessionStorage) {
  62. vAPI.messaging.send('contentscript.js', {
  63. what: 'contentScriptHasLocalStorage',
  64. originURL: window.location.origin
  65. }, localStorageHandler);
  66. }
  67. // TODO: indexedDB
  68. //if ( window.indexedDB && !!window.indexedDB.webkitGetDatabaseNames ) {
  69. // var db = window.indexedDB.webkitGetDatabaseNames().onsuccess = function(sender) {
  70. // console.debug('webkitGetDatabaseNames(): result=%o', sender.target.result);
  71. // };
  72. //}
  73. // TODO: Web SQL
  74. // if ( window.openDatabase ) {
  75. // Sad:
  76. // "There is no way to enumerate or delete the databases available for an origin from this API."
  77. // Ref.: http://www.w3.org/TR/webdatabase/#databases
  78. // }
  79. } catch (e) {
  80. }
  81. })();
  82. // https://github.com/gorhill/uMatrix/issues/45
  83. let collapser = (function () {
  84. let resquestIdGenerator = 1;
  85. let processTimer;
  86. let toProcess = [];
  87. let toFilter = [];
  88. let toCollapse = new Map();
  89. let cachedBlockedMap;
  90. let cachedBlockedMapHash;
  91. let cachedBlockedMapTimer;
  92. let reURLPlaceholder = /\{\{url\}\}/g;
  93. let src1stProps = {
  94. embed: 'src',
  95. iframe: 'src',
  96. img: 'src',
  97. object: 'data',
  98. };
  99. let src2ndProps = {
  100. img: 'srcset',
  101. };
  102. let tagToTypeMap = {
  103. embed: 'media',
  104. iframe: 'frame',
  105. img: 'image',
  106. object: 'media',
  107. };
  108. let cachedBlockedSetClear = function () {
  109. cachedBlockedMap =
  110. cachedBlockedMapHash =
  111. cachedBlockedMapTimer = undefined;
  112. };
  113. // https://github.com/chrisaljoudi/uBlock/issues/174
  114. // Do not remove fragment from src URL
  115. let onProcessed = function (response) {
  116. if (!response) { // This happens if uBO is disabled or restarted.
  117. toCollapse.clear();
  118. return;
  119. }
  120. let targets = toCollapse.get(response.id);
  121. if (targets === undefined) {
  122. return;
  123. }
  124. toCollapse.delete(response.id);
  125. if (cachedBlockedMapHash !== response.hash) {
  126. cachedBlockedMap = new Map(response.blockedResources);
  127. cachedBlockedMapHash = response.hash;
  128. if (cachedBlockedMapTimer !== undefined) {
  129. clearTimeout(cachedBlockedMapTimer);
  130. }
  131. cachedBlockedMapTimer =
  132. vAPI.setTimeout(cachedBlockedSetClear, 30000);
  133. }
  134. if (cachedBlockedMap === undefined || cachedBlockedMap.size === 0) {
  135. return;
  136. }
  137. let placeholders = response.placeholders;
  138. for (let target of targets) {
  139. let tag = target.localName;
  140. let prop = src1stProps[tag];
  141. if (prop === undefined) {
  142. continue;
  143. }
  144. let src = target[prop];
  145. if (typeof src !== 'string' || src.length === 0) {
  146. prop = src2ndProps[tag];
  147. if (prop === undefined) {
  148. continue;
  149. }
  150. src = target[prop];
  151. if (typeof src !== 'string' || src.length === 0) {
  152. continue;
  153. }
  154. }
  155. let collapsed = cachedBlockedMap.get(tagToTypeMap[tag]
  156. + ' '
  157. + src);
  158. if (collapsed === undefined) {
  159. continue;
  160. }
  161. if (collapsed) {
  162. target.style.setProperty('display', 'none', 'important');
  163. target.hidden = true;
  164. continue;
  165. }
  166. switch (tag) {
  167. case 'iframe':
  168. if (placeholders.frame !== true) {
  169. break;
  170. }
  171. let docurl = 'data:text/html,'
  172. + encodeURIComponent(placeholders
  173. .frameDocument
  174. .replace(reURLPlaceholder, src));
  175. let replaced = false;
  176. // Using contentWindow.location prevent tainting browser
  177. // history -- i.e. breaking back button (seen on Chromium).
  178. if (target.contentWindow) {
  179. try {
  180. target.contentWindow.location.replace(docurl);
  181. replaced = true;
  182. } catch(ex) {
  183. }
  184. }
  185. if (!replaced) {
  186. target.setAttribute('src', docurl);
  187. }
  188. break;
  189. case 'img':
  190. if (placeholders.image !== true) {
  191. break;
  192. }
  193. target.style.setProperty('display', 'inline-block');
  194. target.style.setProperty('min-width', '20px', 'important');
  195. target.style.setProperty('min-height', '20px', 'important');
  196. target.style.setProperty('border', placeholders.imageBorder,
  197. 'important');
  198. target.style.setProperty('background',
  199. placeholders.imageBackground,
  200. 'important');
  201. break;
  202. }
  203. }
  204. };
  205. let send = function () {
  206. processTimer = undefined;
  207. toCollapse.set(resquestIdGenerator, toProcess);
  208. let msg = {
  209. what: 'lookupBlockedCollapsibles',
  210. id: resquestIdGenerator,
  211. toFilter: toFilter,
  212. hash: cachedBlockedMapHash
  213. };
  214. vAPI.messaging.send('contentscript.js', msg, onProcessed);
  215. toProcess = [];
  216. toFilter = [];
  217. resquestIdGenerator += 1;
  218. };
  219. let process = function (delay) {
  220. if (toProcess.length === 0) {
  221. return;
  222. }
  223. if (delay === 0) {
  224. if (processTimer !== undefined) {
  225. clearTimeout(processTimer);
  226. }
  227. send();
  228. } else if (processTimer === undefined) {
  229. processTimer = vAPI.setTimeout(send, delay || 47);
  230. }
  231. };
  232. let add = function (target) {
  233. toProcess.push(target);
  234. };
  235. let addMany = function (targets) {
  236. for (let i=targets.length-1; i>=0; --i) {
  237. toProcess.push(targets[i]);
  238. }
  239. };
  240. let iframeSourceModified = function (mutations) {
  241. for (let i=mutations.length-1; i>=0; --i) {
  242. addIFrame(mutations[i].target, true);
  243. }
  244. process();
  245. };
  246. let iframeSourceObserver;
  247. let iframeSourceObserverOptions = {
  248. attributes: true,
  249. attributeFilter: [ 'src' ]
  250. };
  251. let addIFrame = function (iframe, dontObserve) {
  252. // https://github.com/gorhill/uBlock/issues/162
  253. // Be prepared to deal with possible change of src attribute.
  254. if (dontObserve !== true) {
  255. if (iframeSourceObserver === undefined) {
  256. iframeSourceObserver =
  257. new MutationObserver(iframeSourceModified);
  258. }
  259. iframeSourceObserver.observe(iframe,
  260. iframeSourceObserverOptions);
  261. }
  262. let src = iframe.src;
  263. if (src === '' || typeof src !== 'string') {
  264. return;
  265. }
  266. if (src.startsWith('http') === false) {
  267. return;
  268. }
  269. toFilter.push({
  270. type: 'frame',
  271. url: iframe.src,
  272. });
  273. add(iframe);
  274. };
  275. let addIFrames = function (iframes) {
  276. for (let i=iframes.length-1; i>=0; --i) {
  277. addIFrame(iframes[i]);
  278. }
  279. };
  280. let addNodeList = function (nodeList) {
  281. for (let i=nodeList.length-1; i>=0; --i) {
  282. let node = nodeList[i];
  283. if (node.nodeType !== 1) {
  284. continue;
  285. }
  286. if (node.localName === 'iframe') {
  287. addIFrame(node);
  288. }
  289. if (node.childElementCount !== 0) {
  290. addIFrames(node.querySelectorAll('iframe'));
  291. }
  292. }
  293. };
  294. let onResourceFailed = function (ev) {
  295. if (tagToTypeMap[ev.target.localName] !== undefined) {
  296. add(ev.target);
  297. process();
  298. }
  299. };
  300. document.addEventListener('error', onResourceFailed, true);
  301. vAPI.shutdown.add(function () {
  302. document.removeEventListener('error', onResourceFailed, true);
  303. if (iframeSourceObserver !== undefined) {
  304. iframeSourceObserver.disconnect();
  305. iframeSourceObserver = undefined;
  306. }
  307. if (processTimer !== undefined) {
  308. clearTimeout(processTimer);
  309. processTimer = undefined;
  310. }
  311. });
  312. return {
  313. addMany: addMany,
  314. addIFrames: addIFrames,
  315. addNodeList: addNodeList,
  316. process: process
  317. };
  318. })();
  319. // Observe changes in the DOM
  320. // Added node lists will be cumulated here before being processed
  321. (function () {
  322. // This fixes http://acid3.acidtests.org/
  323. if (!document.body) {
  324. return;
  325. }
  326. let addedNodeLists = [];
  327. let addedNodeListsTimer;
  328. let treeMutationObservedHandler = function () {
  329. addedNodeListsTimer = undefined;
  330. for (let i=addedNodeLists.length-1; i>=0; --i) {
  331. collapser.addNodeList(addedNodeLists[i]);
  332. }
  333. collapser.process();
  334. addedNodeLists = [];
  335. };
  336. // https://github.com/gorhill/uBlock/issues/205
  337. // Do not handle added node directly from within mutation observer.
  338. let treeMutationObservedHandlerAsync = function (mutations) {
  339. for (let i=mutations.length-1; i>=0; --i) {
  340. let nodeList = mutations[i].addedNodes;
  341. if (nodeList.length !== 0) {
  342. addedNodeLists.push(nodeList);
  343. }
  344. }
  345. if (addedNodeListsTimer === undefined) {
  346. addedNodeListsTimer =
  347. vAPI.setTimeout(treeMutationObservedHandler, 47);
  348. }
  349. };
  350. // https://github.com/gorhill/httpswitchboard/issues/176
  351. let treeObserver =
  352. new MutationObserver(treeMutationObservedHandlerAsync);
  353. treeObserver.observe(document.body, {
  354. childList: true,
  355. subtree: true
  356. });
  357. vAPI.shutdown.add(function () {
  358. if (addedNodeListsTimer !== undefined) {
  359. clearTimeout(addedNodeListsTimer);
  360. addedNodeListsTimer = undefined;
  361. }
  362. if (treeObserver !== null) {
  363. treeObserver.disconnect();
  364. treeObserver = undefined;
  365. }
  366. addedNodeLists = [];
  367. });
  368. })();
  369. // Executed only once.
  370. //
  371. // https://github.com/gorhill/httpswitchboard/issues/25
  372. //
  373. // https://github.com/gorhill/httpswitchboard/issues/131
  374. // Looks for inline javascript also in at least one a[href] element.
  375. //
  376. // https://github.com/gorhill/uMatrix/issues/485
  377. // Mind "on..." attributes.
  378. //
  379. // https://github.com/gorhill/uMatrix/issues/924
  380. // Report inline styles.
  381. (function () {
  382. if (document.querySelector('script:not([src])') !== null
  383. || document.querySelector('a[href^="javascript:"]') !== null
  384. || document.querySelector('[onabort],[onblur],[oncancel],'
  385. + '[oncanplay],[oncanplaythrough],'
  386. + '[onchange],[onclick],[onclose],'
  387. + '[oncontextmenu],[oncuechange],'
  388. + '[ondblclick],[ondrag],[ondragend],'
  389. + '[ondragenter],[ondragexit],'
  390. + '[ondragleave],[ondragover],'
  391. + '[ondragstart],[ondrop],'
  392. + '[ondurationchange],[onemptied],'
  393. + '[onended],[onerror],[onfocus],'
  394. + '[oninput],[oninvalid],[onkeydown],'
  395. + '[onkeypress],[onkeyup],[onload],'
  396. + '[onloadeddata],[onloadedmetadata],'
  397. + '[onloadstart],[onmousedown],'
  398. + '[onmouseenter],[onmouseleave],'
  399. + '[onmousemove],[onmouseout],'
  400. + '[onmouseover],[onmouseup],[onwheel],'
  401. + '[onpause],[onplay],[onplaying],'
  402. + '[onprogress],[onratechange],[onreset],'
  403. + '[onresize],[onscroll],[onseeked],'
  404. + '[onseeking],[onselect],[onshow],'
  405. + '[onstalled],[onsubmit],[onsuspend],'
  406. + '[ontimeupdate],[ontoggle],'
  407. + '[onvolumechange],[onwaiting],'
  408. + '[onafterprint],[onbeforeprint],'
  409. + '[onbeforeunload],[onhashchange],'
  410. + '[onlanguagechange],[onmessage],'
  411. + '[onoffline],[ononline],[onpagehide],'
  412. + '[onpageshow],[onrejectionhandled],'
  413. + '[onpopstate],[onstorage],'
  414. + '[onunhandledrejection],[onunload],'
  415. + '[oncopy],[oncut],[onpaste]') !== null) {
  416. vAPI.messaging.send('contentscript.js', {
  417. what: 'securityPolicyViolation',
  418. directive: 'script-src',
  419. documentURI: window.location.href
  420. });
  421. }
  422. if (document.querySelector('style,[style]') !== null) {
  423. vAPI.messaging.send('contentscript.js', {
  424. what: 'securityPolicyViolation',
  425. directive: 'style-src',
  426. documentURI: window.location.href
  427. });
  428. }
  429. collapser.addMany(document.querySelectorAll('img'));
  430. collapser.addIFrames(document.querySelectorAll('iframe'));
  431. collapser.process();
  432. })();
  433. // Executed only once.
  434. // https://github.com/gorhill/uMatrix/issues/232
  435. // Force `display` property, Firefox is still affected by the issue.
  436. (function () {
  437. let noscripts = document.querySelectorAll('noscript');
  438. if (noscripts.length === 0) {
  439. return;
  440. }
  441. let redirectTimer;
  442. let reMetaContent = /^\s*(\d+)\s*;\s*url=(['"]?)([^'"]+)\2/i;
  443. let reSafeURL = /^https?:\/\//;
  444. let autoRefresh = function (root) {
  445. let meta =
  446. root.querySelector('meta[http-equiv="refresh"][content]');
  447. if (meta === null) {
  448. return;
  449. }
  450. let match = reMetaContent.exec(meta.getAttribute('content'));
  451. if (match === null || match[3].trim() === '') {
  452. return;
  453. }
  454. let url = new URL(match[3], document.baseURI);
  455. if (reSafeURL.test(url.href) === false) {
  456. return;
  457. }
  458. redirectTimer = setTimeout(function () {
  459. location.assign(url.href);
  460. }, parseInt(match[1], 10) * 1000 + 1);
  461. meta.parentNode.removeChild(meta);
  462. };
  463. let morphNoscript = function (from) {
  464. if (/^application\/(?:xhtml\+)?xml/.test(document.contentType)) {
  465. let to = document.createElement('span');
  466. while (from.firstChild !== null) {
  467. to.appendChild(from.firstChild);
  468. }
  469. return to;
  470. }
  471. let parser = new DOMParser();
  472. let doc =
  473. parser.parseFromString('<span>' + from.textContent + '</span>',
  474. 'text/html');
  475. return document.adoptNode(doc.querySelector('span'));
  476. };
  477. let renderNoscriptTags = function (response) {
  478. if (response !== true) {
  479. return;
  480. }
  481. for (let noscript of noscripts) {
  482. let parent = noscript.parentNode;
  483. if (parent === null) {
  484. continue;
  485. }
  486. let span = morphNoscript(noscript);
  487. span.style.setProperty('display', 'inline', 'important');
  488. if (redirectTimer === undefined) {
  489. autoRefresh(span);
  490. }
  491. parent.replaceChild(span, noscript);
  492. }
  493. };
  494. vAPI.messaging.send('contentscript.js', {
  495. what: 'mustRenderNoscriptTags?'
  496. }, renderNoscriptTags);
  497. })();
  498. vAPI.messaging.send('contentscript.js', {
  499. what: 'shutdown?'
  500. }, function (response) {
  501. if (response === true) {
  502. vAPI.shutdown.exec();
  503. }
  504. });
  505. })();