contentscript.js 20 KB

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