contentscript.js 18 KB

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