/******************************************************************************* ηMatrix - a browser extension to black/white list requests. Copyright (C) 2014-2019 Raymond Hill Copyright (C) 2019-2020 Alessio Vanni This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see {http://www.gnu.org/licenses/}. Home: https://libregit.spks.xyz/heckyel/ematrix uMatrix Home: https://github.com/gorhill/uMatrix */ 'use strict'; // Injected into content pages (function () { // https://github.com/chrisaljoudi/uBlock/issues/464 // https://github.com/gorhill/uMatrix/issues/621 if (document instanceof HTMLDocument === false && document instanceof XMLDocument === false) { return; } // This can also happen (for example if script injected into a // `data:` URI doc) if (!window.location) { return; } // This can happen if (typeof vAPI !== 'object') { //console.debug('contentscript.js > vAPI not found'); return; } // https://github.com/chrisaljoudi/uBlock/issues/456 // Already injected? if (vAPI.contentscriptEndInjected) { //console.debug('contentscript.js > content script already injected'); return; } vAPI.contentscriptEndInjected = true; // Executed only once. (function () { let localStorageHandler = function (mustRemove) { if (mustRemove) { window.localStorage.clear(); window.sessionStorage.clear(); } }; // Check with extension whether local storage must be emptied // rhill 2014-03-28: we need an exception handler in case // 3rd-party access to site data is disabled. // https://github.com/gorhill/httpswitchboard/issues/215 try { let hasLocalStorage = window.localStorage && window.localStorage.length !== 0; let hasSessionStorage = window.sessionStorage && window.sessionStorage.length !== 0; if (hasLocalStorage || hasSessionStorage) { vAPI.messaging.send('contentscript.js', { what: 'contentScriptHasLocalStorage', originURL: window.location.origin }, localStorageHandler); } // TODO: indexedDB //if ( window.indexedDB && !!window.indexedDB.webkitGetDatabaseNames ) { // var db = window.indexedDB.webkitGetDatabaseNames().onsuccess = function(sender) { // console.debug('webkitGetDatabaseNames(): result=%o', sender.target.result); // }; //} // TODO: Web SQL // if ( window.openDatabase ) { // Sad: // "There is no way to enumerate or delete the databases available for an origin from this API." // Ref.: http://www.w3.org/TR/webdatabase/#databases // } } catch (e) { } })(); // https://github.com/gorhill/uMatrix/issues/45 let collapser = (function () { let resquestIdGenerator = 1; let processTimer; let toProcess = []; let toFilter = []; let toCollapse = new Map(); let cachedBlockedMap; let cachedBlockedMapHash; let cachedBlockedMapTimer; let reURLPlaceholder = /\{\{url\}\}/g; let src1stProps = { embed: 'src', iframe: 'src', img: 'src', object: 'data', }; let src2ndProps = { img: 'srcset', }; let tagToTypeMap = { embed: 'media', iframe: 'frame', img: 'image', object: 'media', }; let cachedBlockedSetClear = function () { cachedBlockedMap = cachedBlockedMapHash = cachedBlockedMapTimer = undefined; }; // https://github.com/chrisaljoudi/uBlock/issues/174 // Do not remove fragment from src URL let onProcessed = function (response) { if (!response) { // This happens if uBO is disabled or restarted. toCollapse.clear(); return; } let targets = toCollapse.get(response.id); if (targets === undefined) { return; } toCollapse.delete(response.id); if (cachedBlockedMapHash !== response.hash) { cachedBlockedMap = new Map(response.blockedResources); cachedBlockedMapHash = response.hash; if (cachedBlockedMapTimer !== undefined) { clearTimeout(cachedBlockedMapTimer); } cachedBlockedMapTimer = vAPI.setTimeout(cachedBlockedSetClear, 30000); } if (cachedBlockedMap === undefined || cachedBlockedMap.size === 0) { return; } let placeholders = response.placeholders; for (let target of targets) { let tag = target.localName; let prop = src1stProps[tag]; if (prop === undefined) { continue; } let src = target[prop]; if (typeof src !== 'string' || src.length === 0) { prop = src2ndProps[tag]; if (prop === undefined) { continue; } src = target[prop]; if (typeof src !== 'string' || src.length === 0) { continue; } } let collapsed = cachedBlockedMap.get(tagToTypeMap[tag] + ' ' + src); if (collapsed === undefined) { continue; } if (collapsed) { target.style.setProperty('display', 'none', 'important'); target.hidden = true; continue; } switch (tag) { case 'iframe': if (placeholders.frame !== true) { break; } let docurl = 'data:text/html,' + encodeURIComponent(placeholders .frameDocument .replace(reURLPlaceholder, src)); let replaced = false; // Using contentWindow.location prevent tainting browser // history -- i.e. breaking back button (seen on Chromium). if (target.contentWindow) { try { target.contentWindow.location.replace(docurl); replaced = true; } catch(ex) { } } if (!replaced) { target.setAttribute('src', docurl); } break; case 'img': if (placeholders.image !== true) { break; } target.style.setProperty('display', 'inline-block'); target.style.setProperty('min-width', '20px', 'important'); target.style.setProperty('min-height', '20px', 'important'); target.style.setProperty('border', placeholders.imageBorder, 'important'); target.style.setProperty('background', placeholders.imageBackground, 'important'); break; } } }; let send = function () { processTimer = undefined; toCollapse.set(resquestIdGenerator, toProcess); let msg = { what: 'lookupBlockedCollapsibles', id: resquestIdGenerator, toFilter: toFilter, hash: cachedBlockedMapHash }; vAPI.messaging.send('contentscript.js', msg, onProcessed); toProcess = []; toFilter = []; resquestIdGenerator += 1; }; let process = function (delay) { if (toProcess.length === 0) { return; } if (delay === 0) { if (processTimer !== undefined) { clearTimeout(processTimer); } send(); } else if (processTimer === undefined) { processTimer = vAPI.setTimeout(send, delay || 47); } }; let add = function (target) { toProcess.push(target); }; let addMany = function (targets) { for (let i=targets.length-1; i>=0; --i) { toProcess.push(targets[i]); } }; let iframeSourceModified = function (mutations) { for (let i=mutations.length-1; i>=0; --i) { addIFrame(mutations[i].target, true); } process(); }; let iframeSourceObserver; let iframeSourceObserverOptions = { attributes: true, attributeFilter: [ 'src' ] }; let addIFrame = function (iframe, dontObserve) { // https://github.com/gorhill/uBlock/issues/162 // Be prepared to deal with possible change of src attribute. if (dontObserve !== true) { if (iframeSourceObserver === undefined) { iframeSourceObserver = new MutationObserver(iframeSourceModified); } iframeSourceObserver.observe(iframe, iframeSourceObserverOptions); } let src = iframe.src; if (src === '' || typeof src !== 'string') { return; } if (src.startsWith('http') === false) { return; } toFilter.push({ type: 'frame', url: iframe.src, }); add(iframe); }; let addIFrames = function (iframes) { for (let i=iframes.length-1; i>=0; --i) { addIFrame(iframes[i]); } }; let addNodeList = function (nodeList) { for (let i=nodeList.length-1; i>=0; --i) { let node = nodeList[i]; if (node.nodeType !== 1) { continue; } if (node.localName === 'iframe') { addIFrame(node); } if (node.childElementCount !== 0) { addIFrames(node.querySelectorAll('iframe')); } } }; let onResourceFailed = function (ev) { if (tagToTypeMap[ev.target.localName] !== undefined) { add(ev.target); process(); } }; document.addEventListener('error', onResourceFailed, true); vAPI.shutdown.add(function () { document.removeEventListener('error', onResourceFailed, true); if (iframeSourceObserver !== undefined) { iframeSourceObserver.disconnect(); iframeSourceObserver = undefined; } if (processTimer !== undefined) { clearTimeout(processTimer); processTimer = undefined; } }); return { addMany: addMany, addIFrames: addIFrames, addNodeList: addNodeList, process: process }; })(); // Observe changes in the DOM // Added node lists will be cumulated here before being processed (function () { // This fixes http://acid3.acidtests.org/ if (!document.body) { return; } let addedNodeLists = []; let addedNodeListsTimer; let treeMutationObservedHandler = function () { addedNodeListsTimer = undefined; for (let i=addedNodeLists.length-1; i>=0; --i) { collapser.addNodeList(addedNodeLists[i]); } collapser.process(); addedNodeLists = []; }; // https://github.com/gorhill/uBlock/issues/205 // Do not handle added node directly from within mutation observer. let treeMutationObservedHandlerAsync = function (mutations) { for (let i=mutations.length-1; i>=0; --i) { let nodeList = mutations[i].addedNodes; if (nodeList.length !== 0) { addedNodeLists.push(nodeList); } } if (addedNodeListsTimer === undefined) { addedNodeListsTimer = vAPI.setTimeout(treeMutationObservedHandler, 47); } }; // https://github.com/gorhill/httpswitchboard/issues/176 let treeObserver = new MutationObserver(treeMutationObservedHandlerAsync); treeObserver.observe(document.body, { childList: true, subtree: true }); vAPI.shutdown.add(function () { if (addedNodeListsTimer !== undefined) { clearTimeout(addedNodeListsTimer); addedNodeListsTimer = undefined; } if (treeObserver !== null) { treeObserver.disconnect(); treeObserver = undefined; } addedNodeLists = []; }); })(); // Executed only once. // // https://github.com/gorhill/httpswitchboard/issues/25 // // https://github.com/gorhill/httpswitchboard/issues/131 // Looks for inline javascript also in at least one a[href] element. // // https://github.com/gorhill/uMatrix/issues/485 // Mind "on..." attributes. // // https://github.com/gorhill/uMatrix/issues/924 // Report inline styles. (function () { if (document.querySelector('script:not([src])') !== null || document.querySelector('a[href^="javascript:"]') !== null || 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) { vAPI.messaging.send('contentscript.js', { what: 'securityPolicyViolation', directive: 'script-src', documentURI: window.location.href }); } if (document.querySelector('style,[style]') !== null) { vAPI.messaging.send('contentscript.js', { what: 'securityPolicyViolation', directive: 'style-src', documentURI: window.location.href }); } collapser.addMany(document.querySelectorAll('img')); collapser.addIFrames(document.querySelectorAll('iframe')); collapser.process(); })(); // Executed only once. // https://github.com/gorhill/uMatrix/issues/232 // Force `display` property, Firefox is still affected by the issue. (function () { let noscripts = document.querySelectorAll('noscript'); if (noscripts.length === 0) { return; } let redirectTimer; let reMetaContent = /^\s*(\d+)\s*;\s*url=(['"]?)([^'"]+)\2/i; let reSafeURL = /^https?:\/\//; let autoRefresh = function (root) { let meta = root.querySelector('meta[http-equiv="refresh"][content]'); if (meta === null) { return; } let match = reMetaContent.exec(meta.getAttribute('content')); if (match === null || match[3].trim() === '') { return; } let url = new URL(match[3], document.baseURI); if (reSafeURL.test(url.href) === false) { return; } redirectTimer = setTimeout(function () { location.assign(url.href); }, parseInt(match[1], 10) * 1000 + 1); meta.parentNode.removeChild(meta); }; let morphNoscript = function (from) { if (/^application\/(?:xhtml\+)?xml/.test(document.contentType)) { let to = document.createElement('span'); while (from.firstChild !== null) { to.appendChild(from.firstChild); } return to; } let parser = new DOMParser(); let doc = parser.parseFromString('' + from.textContent + '', 'text/html'); return document.adoptNode(doc.querySelector('span')); }; let renderNoscriptTags = function (response) { if (response !== true) { return; } for (let noscript of noscripts) { let parent = noscript.parentNode; if (parent === null) { continue; } let span = morphNoscript(noscript); span.style.setProperty('display', 'inline', 'important'); if (redirectTimer === undefined) { autoRefresh(span); } parent.replaceChild(span, noscript); } }; vAPI.messaging.send('contentscript.js', { what: 'mustRenderNoscriptTags?' }, renderNoscriptTags); })(); vAPI.messaging.send('contentscript.js', { what: 'shutdown?' }, function (response) { if (response === true) { vAPI.shutdown.exec(); } }); })();