/******************************************************************************* uMatrix - a browser extension to black/white list requests. Copyright (C) 2014-present Raymond Hill 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://github.com/gorhill/uMatrix */ /* global HTMLDocument, XMLDocument */ 'use strict'; /******************************************************************************/ /******************************************************************************/ // Injected into content pages (( ) => { /******************************************************************************/ // 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. { const 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 { const hasLocalStorage = window.localStorage && window.localStorage.length !== 0; const hasSessionStorage = window.sessionStorage && window.sessionStorage.length !== 0; if ( hasLocalStorage || hasSessionStorage ) { vAPI.messaging.send('contentscript.js', { what: 'contentScriptHasLocalStorage', originURL: window.location.origin, }).then(response => { localStorageHandler(response); }); } // 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 const collapser = (( ) => { let resquestIdGenerator = 1, processTimer, toProcess = [], toFilter = [], cachedBlockedMap, cachedBlockedMapHash, cachedBlockedMapTimer; const toCollapse = new Map(); const reURLPlaceholder = /\{\{url\}\}/g; const src1stProps = { 'embed': 'src', 'frame': 'src', 'iframe': 'src', 'img': 'src', 'object': 'data' }; const src2ndProps = { 'img': 'srcset' }; const tagToTypeMap = { embed: 'media', frame: 'frame', iframe: 'frame', img: 'image', object: 'media' }; const cachedBlockedSetClear = function() { cachedBlockedMap = cachedBlockedMapHash = cachedBlockedMapTimer = undefined; }; // https://github.com/chrisaljoudi/uBlock/issues/174 // Do not remove fragment from src URL const onProcessed = function(response) { if ( !response ) { // This happens if uBO is disabled or restarted. toCollapse.clear(); return; } const 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; } const placeholders = response.placeholders; for ( const target of targets ) { const 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; } } const 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 'frame': case 'iframe': if ( placeholders.frame !== true ) { break; } const 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; } // Do not insert placeholder if the image was actually loaded. // This can happen if an allow rule was created while the // document was loading. if ( target.complete && target.naturalWidth !== 0 && target.naturalHeight !== 0 ) { 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; } } }; const send = function() { processTimer = undefined; toCollapse.set(resquestIdGenerator, toProcess); vAPI.messaging.send('contentscript.js', { what: 'lookupBlockedCollapsibles', id: resquestIdGenerator, toFilter: toFilter, hash: cachedBlockedMapHash, }).then(response => { onProcessed(response); }); toProcess = []; toFilter = []; resquestIdGenerator += 1; }; const 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); } }; const add = function(target) { toProcess.push(target); }; var addMany = function(targets) { var i = targets.length; while ( i-- ) { toProcess.push(targets[i]); } }; const iframeSourceModified = function(mutations) { let i = mutations.length; while ( i-- ) { addIFrame(mutations[i].target, true); } process(); }; let iframeSourceObserver; const iframeSourceObserverOptions = { attributes: true, attributeFilter: [ 'src' ], }; const 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); } const src = iframe.src; if ( src === '' || typeof src !== 'string' ) { return; } if ( src.startsWith('http') === false ) { return; } toFilter.push({ type: 'frame', url: iframe.src }); add(iframe); }; const addIFrames = function(iframes) { let i = iframes.length; while ( i-- ) { addIFrame(iframes[i]); } }; const addNodeList = function(nodeList) { let i = nodeList.length; while ( i-- ) { const node = nodeList[i]; if ( node.nodeType !== 1 ) { continue; } if ( node.localName === 'iframe' || node.localName === 'frame' ) { addIFrame(node); } if ( node.childElementCount !== 0 ) { addIFrames(node.querySelectorAll('iframe, frame')); } } }; const 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, addIFrames, addNodeList, process, }; })(); /******************************************************************************/ /******************************************************************************/ // Observe changes in the DOM // Added node lists will be cumulated here before being processed (( ) => { // This fixes http://acid3.acidtests.org/ if ( !document.body ) { return; } let addedNodeLists = []; let addedNodeListsTimer; const treeMutationObservedHandler = function() { addedNodeListsTimer = undefined; let i = addedNodeLists.length; while ( 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. const treeMutationObservedHandlerAsync = function(mutations) { let iMutation = mutations.length; while ( iMutation-- ) { const nodeList = mutations[iMutation].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. { 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, frame')); collapser.process(); } /******************************************************************************/ /******************************************************************************/ // Executed only once. // https://github.com/gorhill/uMatrix/issues/232 // Force `display` property, Firefox is still affected by the issue. (( ) => { const noscripts = document.querySelectorAll('noscript'); if ( noscripts.length === 0 ) { return; } const reMetaContent = /^\s*(\d+)\s*;\s*url=(['"]?)([^'"]+)\2/i; const reSafeURL = /^https?:\/\//; let redirectTimer; const autoRefresh = function(root) { const meta = root.querySelector('meta[http-equiv="refresh"][content]'); if ( meta === null ) { return; } const match = reMetaContent.exec(meta.getAttribute('content')); if ( match === null || match[3].trim() === '' ) { return; } const url = new URL(match[3], document.baseURI); if ( reSafeURL.test(url.href) === false ) { return; } redirectTimer = setTimeout( ( ) => { location.assign(url.href); }, parseInt(match[1], 10) * 1000 + 1 ); meta.parentNode.removeChild(meta); }; const morphNoscript = function(from) { if ( /^application\/(?:xhtml\+)?xml/.test(document.contentType) ) { const to = document.createElement('span'); while ( from.firstChild !== null ) { to.appendChild(from.firstChild); } return to; } const parser = new DOMParser(); const doc = parser.parseFromString( '' + from.textContent + '', 'text/html' ); return document.adoptNode(doc.querySelector('span')); }; const renderNoscriptTags = function(response) { if ( response !== true ) { return; } for ( var noscript of noscripts ) { const parent = noscript.parentNode; if ( parent === null ) { continue; } const 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?', }).then(response => { renderNoscriptTags(response); }); })(); /******************************************************************************/ /******************************************************************************/ vAPI.messaging.send('contentscript.js', { what: 'shutdown?', }).then(response => { if ( response === true ) { vAPI.shutdown.exec(); } }); /******************************************************************************/ /******************************************************************************/ })();