Skip to content

Instantly share code, notes, and snippets.

@wellington1993
Created October 7, 2019 13:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wellington1993/d74a79143c0a45d10641bc70ce7c452a to your computer and use it in GitHub Desktop.
Save wellington1993/d74a79143c0a45d10641bc70ce7c452a to your computer and use it in GitHub Desktop.
moz-extension://28cb16e6-ec95-403c-b237-b21bfda305ff/lib/content-script/preload.js
/**
* This file is part of AdBlocker Ultimate Browser Extension
*
* AdBlocker Ultimate Browser Extension is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AdBlocker Ultimate Browser Extension 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with AdBlocker Ultimate Browser Extension. If not, see <http://www.gnu.org/licenses/>.
*/
/* global contentPage, ExtendedCss, HTMLDocument, XMLDocument, ElementCollapser, CssHitsCounter */
(function () {
var requestTypeMap = {
"img": "IMAGE",
"input": "IMAGE",
"audio": "MEDIA",
"video": "MEDIA",
"object": "OBJECT",
"frame": "SUBDOCUMENT",
"iframe": "SUBDOCUMENT",
"embed": "OBJECT"
};
// Don't apply scripts twice
var scriptsApplied = false;
/**
* Do not use shadow DOM on some websites
* https://code.google.com/p/chromium/issues/detail?id=496055
*/
var shadowDomExceptions = [
'mail.google.com',
'inbox.google.com',
'productforums.google.com'
];
/**
* Do not use iframes pre-hiding on some websites.
* https://github.com/AdguardTeam/AdguardBrowserExtension/issues/720
*/
var iframeHidingExceptions = [
'docs.google.com'
];
var collapseRequests = Object.create(null);
var collapseRequestId = 1;
var isFirefox = false;
var isOpera = false;
var loadTruncatedCss = false;
/**
* Set callback for saving css hits
*/
if (typeof CssHitsCounter !== 'undefined' &&
typeof CssHitsCounter.setCssHitsFoundCallback === 'function') {
CssHitsCounter.setCssHitsFoundCallback(function (stats) {
contentPage.sendMessage({type: 'saveCssHitStats', stats: stats});
});
}
/**
* When Background page receives 'onCommitted' frame event then it sends scripts to corresponding frame
* It allows us to execute script as soon as possible, because runtime.messaging makes huge overhead
* If onCommitted event doesn't occur for the frame, scripts will be applied in usual way.
*/
contentPage.onMessage.addListener(function (response, sender, sendResponse) {
if (response.type === 'injectScripts') {
// Notify background-page that content-script was received scripts
sendResponse({applied: true});
if (!isHtml()) {
return;
}
applyScripts(response.scripts);
}
});
/**
* Initializing content script
*/
var init = function () {
if (!isHtml()) {
return;
}
initRequestWrappers();
var userAgent = navigator.userAgent.toLowerCase();
isFirefox = userAgent.indexOf('firefox') > -1;
isOpera = userAgent.indexOf('opera') > -1 || userAgent.indexOf('opr') > -1;
if (window !== window.top) {
var width = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
var height = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
// Load only small set of css for small frames.
// We hide all generic css rules in this case.
// https://github.com/AdguardTeam/AdguardBrowserExtension/issues/223
loadTruncatedCss = (height * width) < 100000;
}
initCollapseEventListeners();
tryLoadCssAndScripts();
};
/**
* Checks if it is html document
*
* @returns {boolean}
*/
var isHtml = function () {
return (document instanceof HTMLDocument) ||
// https://github.com/AdguardTeam/AdguardBrowserExtension/issues/233
((document instanceof XMLDocument) && (document.createElement('div') instanceof HTMLDivElement));
};
/**
* Uses in `initRequestWrappers` method.
* We insert wrapper's code into http/https documents and dynamically created frames.
* The last one is due to the circumvention with using iframe's contentWindow.
*/
var isHttpOrAboutPage = function () {
var protocol = window.location.protocol;
return protocol.indexOf('http') === 0 || protocol.indexOf('about:') === 0;
};
/**
* Try to keep DOM clean: let script removes itself when execution completes
* @returns {string}
*/
var cleanupCurrentScriptToString = function () {
var cleanup = function () {
var current = document.currentScript;
var parent = current && current.parentNode;
if (parent) {
parent.removeChild(current);
}
};
return '(' + cleanup.toString() + ')();';
};
/**
* Execute scripts in a page context and cleanup itself when execution completes
* @param scripts Array of scripts to execute
*/
var executeScripts = function (scripts) {
if (!scripts || scripts.length === 0) {
return;
}
// Wraps with try catch and appends cleanup
scripts.unshift("try {");
scripts.push("} catch (ex) { console.error('Error executing AG js: ' + ex); }");
scripts.push(cleanupCurrentScriptToString());
var script = document.createElement("script");
script.setAttribute("type", "text/javascript");
script.textContent = scripts.join("\r\n");
(document.head || document.documentElement).appendChild(script);
};
/**
* We should override WebSocket constructor in the following browsers: Chrome (between 47 and 57 versions), Edge, YaBrowser, Opera and Safari (old versions)
* Firefox and Safari (9 or higher) can be omitted because they allow us to inspect and block WS requests.
* This function simply checks the conditions above.
* @returns true if WebSocket constructor should be overridden
*/
var shouldOverrideWebSocket = function () {
// Checks for using of Content Blocker API for Safari 9+
if (contentPage.isSafari) {
return !contentPage.isSafariContentBlockerEnabled;
}
var userAgent = navigator.userAgent.toLowerCase();
var isFirefox = userAgent.indexOf('firefox') >= 0;
// Explicit check, we must not go further in case of Firefox
// https://github.com/AdguardTeam/AdguardBrowserExtension/issues/379
if (isFirefox) {
return false;
}
// Keep in mind that the following browsers (that support WebExt-API) Chrome, Edge, YaBrowser and Opera contain `Chrome/<version>` in their User-Agent string.
var cIndex = userAgent.indexOf('chrome/');
if (cIndex < 0) {
return false;
}
var version = userAgent.substring(cIndex + 7);
var versionNumber = Number.parseInt(version.substring(0, version.indexOf('.')));
// WebSockets are broken in old versions of chrome and we don't need this hack in new version cause then websocket traffic is intercepted
return versionNumber >= 47 && versionNumber <= 57;
};
/**
* We should override RTCPeerConnection in all browsers, except the case of using of Content Blocker API for Safari 9+
* @returns true if RTCPeerConnection should be overridden
*/
var shouldOverrideWebRTC = function () {
// Checks for using of Content Blocker API for Safari 9+
if (contentPage.isSafari) {
return !contentPage.isSafariContentBlockerEnabled;
}
return true;
};
/**
* Overrides window.WebSocket and window.RTCPeerConnection running the function from wrappers.js
* https://github.com/AdguardTeam/AdguardBrowserExtension/issues/203
* https://github.com/AdguardTeam/AdguardBrowserExtension/issues/588
*/
/* global injectPageScriptAPI, initPageMessageListener */
var initRequestWrappers = function () {
// Only for dynamically created frames and http/https documents.
if (!isHttpOrAboutPage()) {
return;
}
/**
*
* The code below is supposed to be used in WebExt extensions.
* This code overrides WebSocket constructor (except for newer Chrome and FF) and RTCPeerConnection constructor, so that we could inspect & block them.
*
* https://github.com/AdguardTeam/AdguardBrowserExtension/issues/273
* https://github.com/AdguardTeam/AdguardBrowserExtension/issues/572
*/
var overrideWebSocket = shouldOverrideWebSocket();
var overrideWebRTC = shouldOverrideWebRTC();
if (overrideWebSocket || overrideWebRTC) {
initPageMessageListener();
var wrapperScriptName = 'wrapper-script-' + Math.random().toString().substr(2);
var script = "(" + injectPageScriptAPI.toString() + ")('" + wrapperScriptName + "', " + overrideWebSocket + ", " + overrideWebRTC + ");";
executeScripts([script]);
}
};
/**
* The main purpose of this function is to prevent blocked iframes "flickering".
* So, we do two things:
* 1. Add a temporary display:none style for all frames (which is removed on DOMContentLoaded event)
* 2. Use a MutationObserver to react on dynamically added iframe
*
* https://github.com/AdguardTeam/AdguardBrowserExtension/issues/301
*/
var addIframeHidingStyle = function () {
if (window !== window.top) {
// Do nothing for frames
return;
}
/**
* Checks for pre-hide iframes exception
*/
var hideIframes = iframeHidingExceptions.indexOf(document.domain) < 0;
var iframeHidingSelector = "iframe[src]";
if (hideIframes) {
ElementCollapser.hideBySelector(iframeHidingSelector, null);
}
/**
* For iframes with changed source we check if it should be collapsed
*
* @param mutations
*/
var handleIframeSrcChanged = function (mutations) {
for (var i = 0; i < mutations.length; i++) {
var iframe = mutations[i].target;
if (iframe) {
checkShouldCollapseElement(iframe);
}
}
};
var iframeObserver = new MutationObserver(handleIframeSrcChanged);
var iframeObserverOptions = {
attributes: true,
attributeFilter: ['src']
};
/**
* Dynamically added frames handler
*
* @param mutations
*/
var handleDomChanged = function (mutations) {
var iframes = [];
for (var i = 0; i < mutations.length; i++) {
var mutation = mutations[i];
var addedNodes = mutation.addedNodes;
for (var j = 0; j < addedNodes.length; j++) {
var node = addedNodes[j];
if (node.localName === "iframe") {
iframes.push(node);
}
}
}
if (iframes.length > 0) {
var iIframes = iframes.length;
while (iIframes--) {
var iframe = iframes[iIframes];
checkShouldCollapseElement(iframe);
iframeObserver.observe(iframe, iframeObserverOptions);
}
}
};
/**
* Removes iframes hide style and initiates should-collapse check for iframes
*/
var onDocumentReady = function () {
var iframes = document.getElementsByTagName('iframe');
for (var i = 0; i < iframes.length; i++) {
checkShouldCollapseElement(iframes[i]);
}
if (hideIframes) {
ElementCollapser.unhideBySelector(iframeHidingSelector);
}
if (document.body) {
// Handle dynamically added frames
var domObserver = new MutationObserver(handleDomChanged);
domObserver.observe(document.body, {
childList: true,
subtree: true
});
}
};
//Document can already be loaded
if (['interactive', 'complete'].indexOf(document.readyState) >= 0) {
onDocumentReady();
} else {
document.addEventListener('DOMContentLoaded', onDocumentReady);
}
};
/**
* Loads CSS and JS injections
*/
var tryLoadCssAndScripts = function () {
var message = {
type: 'getSelectorsAndScripts',
documentUrl: window.location.href,
options: {
filter: ['selectors', 'scripts'],
genericHide: loadTruncatedCss
}
};
/**
* Sending message to background page and passing a callback function
*/
contentPage.sendMessage(message, processCssAndScriptsResponse);
};
/**
* Processes response from the background page containing CSS and JS injections
*
* @param response Response from the background page
*/
var processCssAndScriptsResponse = function (response) {
if (!response || response.requestFilterReady === false) {
/**
* This flag (requestFilterReady) means that we should wait for a while, because the
* request filter is not ready yet. This is possible only on browser startup.
* In this case we'll delay injections until extension is fully initialized.
*/
setTimeout(function () {
tryLoadCssAndScripts();
}, 100);
return;
} else if (response.collapseAllElements) {
/**
* This flag (collapseAllElements) means that we should check all page elements
* and collapse them if needed. Why? On browser startup we can't block some
* ad/tracking requests because extension is not yet initialized when
* these requests are executed. At least we could hide these elements.
*/
applySelectors(response.selectors);
applyScripts(response.scripts);
initBatchCollapse();
} else {
applySelectors(response.selectors);
applyScripts(response.scripts);
}
if (response && response.selectors && response.selectors.css && response.selectors.css.length > 0) {
addIframeHidingStyle();
}
if (typeof CssHitsCounter !== 'undefined' &&
typeof CssHitsCounter.count === 'function' &&
response && response.selectors && response.selectors.cssHitsCounterEnabled) {
// Start css hits calculation
CssHitsCounter.count();
}
};
/**
* Sets "style" DOM element content.
*
* @param styleEl "style" DOM element
* @param cssContent CSS content to set
*/
var setStyleContent = function (styleEl, cssContent) {
cssContent = cssContent.replace(new RegExp('::content ', 'g'), '');
styleEl.textContent = cssContent;
};
/**
* Applies CSS and extended CSS stylesheets
*
* @param selectors Object with the stylesheets got from the background page.
*/
var applySelectors = function (selectors) {
if (!selectors) {
return;
}
applyCss(selectors.css);
applyExtendedCss(selectors.extendedCss);
};
/**
* Applies CSS stylesheets
*
* @param css Array with CSS stylesheets
*/
var applyCss = function (css) {
if (!css || css.length === 0) {
return;
}
for (var i = 0; i < css.length; i++) {
var styleEl = document.createElement("style");
styleEl.setAttribute("type", "text/css");
setStyleContent(styleEl, css[i]);
(document.head || document.documentElement).appendChild(styleEl);
protectStyleElementFromRemoval(styleEl);
protectStyleElementContent(styleEl);
}
};
/**
* Applies Extended Css stylesheet
*
* @param extendedCss Array with ExtendedCss stylesheets
*/
var applyExtendedCss = function (extendedCss) {
if (!extendedCss || !extendedCss.length) {
return;
}
// https://github.com/AdguardTeam/ExtendedCss
new ExtendedCss(extendedCss.join("\n")).apply();
};
/**
* Protects specified style element from changes to the current document
* Add a mutation observer, which is adds our rules again if it was removed
*
* @param protectStyleEl protected style element
*/
var protectStyleElementContent = function (protectStyleEl) {
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
if (!MutationObserver) {
return;
}
/* observer, which observe protectStyleEl inner changes, without deleting styleEl */
var innerObserver = new MutationObserver(function (mutations) {
for (var i = 0; i < mutations.length; i++) {
var m = mutations[i];
if (protectStyleEl.hasAttribute("mod") && protectStyleEl.getAttribute("mod") == "inner") {
protectStyleEl.removeAttribute("mod");
break;
}
protectStyleEl.setAttribute("mod", "inner");
var isProtectStyleElModified = false;
/* further, there are two mutually exclusive situations: either there were changes the text of protectStyleEl,
either there was removes a whole child "text" element of protectStyleEl
we'll process both of them */
if (m.removedNodes.length > 0) {
for (var j = 0; j < m.removedNodes.length; j++) {
isProtectStyleElModified = true;
protectStyleEl.appendChild(m.removedNodes[j]);
}
} else {
if (m.oldValue) {
isProtectStyleElModified = true;
protectStyleEl.textContent = m.oldValue;
}
}
if (!isProtectStyleElModified) {
protectStyleEl.removeAttribute("mod");
}
}
});
innerObserver.observe(protectStyleEl, {
'childList': true,
'characterData': true,
'subtree': true,
'characterDataOldValue': true
});
};
/**
* Protects style element from removing.
*
* @param protectStyleEl protected style element
*/
var protectStyleElementFromRemoval = function (protectStyleEl) {
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
if (!MutationObserver) {
return;
}
/* observer, which observe deleting protectStyleEl */
var outerObserver = new MutationObserver(function (mutations) {
for (var i = 0; i < mutations.length; i++) {
var m = mutations[i];
var removedNodeIndex = [].indexOf.call(mutations[i].removedNodes, protectStyleEl);
if (removedNodeIndex != -1) {
var removedStyleEl = m.removedNodes[removedNodeIndex];
outerObserver.disconnect();
applyCss([removedStyleEl.textContent]);
break;
}
}
});
outerObserver.observe(protectStyleEl.parentNode, {'childList': true, 'characterData': true});
};
/**
* Applies JS injections.
*
* @param scripts Array with JS scripts and scriptSource ('remote' or 'local')
*/
var applyScripts = function (scripts) {
if (scriptsApplied) {
return;
}
scriptsApplied = true;
if (!scripts || scripts.length === 0) {
return;
}
var scriptsToApply = [];
for (var i = 0; i < scripts.length; i++) {
var scriptRule = scripts[i];
switch (scriptRule.scriptSource) {
case 'local':
scriptsToApply.push(scriptRule.rule);
break;
case 'remote':
/**
* Note (!) (Firefox, Opera):
* In case of Firefox and Opera add-ons, JS filtering rules are hardcoded into add-on code.
* Look at ScriptFilterRule.getScriptSource to learn more.
*/
if (!isOpera) {
scriptsToApply.push(scriptRule.rule);
}
break;
}
}
if (scriptsToApply.length === 0) {
return;
}
/**
* JS injections are created by JS filtering rules:
* http://adguard.com/en/filterrules.html#javascriptInjection
*/
executeScripts(scriptsToApply);
};
/**
* Init listeners for error and load events.
* We will then check loaded elements if they are blocked by our extension.
* In this case we'll hide these blocked elements.
*/
var initCollapseEventListeners = function () {
document.addEventListener("error", checkShouldCollapse, true);
// We need to listen for load events to hide blocked iframes (they don't raise error event)
document.addEventListener("load", checkShouldCollapse, true);
};
/**
* Checks if loaded element is blocked by AG and should be hidden
*
* @param event Load or error event
*/
var checkShouldCollapse = function (event) {
var element = event.target;
var eventType = event.type;
var tagName = element.tagName.toLowerCase();
var expectedEventType = (tagName == "iframe" || tagName == "frame" || tagName == "embed") ? "load" : "error";
if (eventType != expectedEventType) {
return;
}
checkShouldCollapseElement(element);
};
/**
* Extracts element URL from the dom node
*
* @param element DOM node
*/
var getElementUrl = function (element) {
var elementUrl = element.src || element.data;
if (!elementUrl ||
elementUrl.indexOf('http') !== 0 ||
// Some sources could not be set yet, lazy loaded images or smth.
// In some cases like on gog.com, collapsing these elements could break the page script loading their sources
elementUrl === element.baseURI) {
return null;
}
return elementUrl;
};
/**
* Saves collapse request (to be reused after we get result from bg page)
*
* @param element Element to check
* @return request ID
*/
var saveCollapseRequest = function (element) {
var tagName = element.tagName.toLowerCase();
var requestId = collapseRequestId++;
collapseRequests[requestId] = {
element: element,
src: element.src,
tagName: tagName
};
return requestId;
};
/**
* Hides element temporarily (until collapse check request is processed)
*
* @param element Element to hide
*/
var tempHideElement = function (element) {
// We skip big frames here
if (element.localName === 'iframe' || element.localName === 'frame' || element.localName === 'embed') {
if (element.clientHeight * element.clientWidth > 400 * 300) {
return;
}
}
ElementCollapser.hideElement(element);
};
/**
* Response callback for "processShouldCollapse" message.
*
* @param response Response got from the background page
*/
var onProcessShouldCollapseResponse = function (response) {
if (!response) {
return;
}
// Get original collapse request
var collapseRequest = collapseRequests[response.requestId];
if (!collapseRequest) {
return;
}
delete collapseRequests[response.requestId];
var element = collapseRequest.element;
if (response.collapse === true) {
var elementUrl = collapseRequest.src;
ElementCollapser.collapseElement(element, elementUrl);
}
// Unhide element, which was previously hidden by "tempHideElement"
// In case if element is collapsed, there's no need to hide it
// Otherwise we shouldn't hide it either as it shouldn't be blocked
ElementCollapser.unhideElement(element);
};
/**
* Checks if element is blocked by AG and should be hidden
*
* @param element Element to check
*/
var checkShouldCollapseElement = function (element) {
var requestType = requestTypeMap[element.localName];
if (!requestType) {
return;
}
var elementUrl = getElementUrl(element);
if (!elementUrl) {
return;
}
// Save request to a map (it will be used in response callback)
var requestId = saveCollapseRequest(element);
// Hide element right away (to prevent iframes "blinking")
tempHideElement(element);
// Send a message to the background page to check if the element really should be collapsed
var message = {
type: 'processShouldCollapse',
elementUrl: elementUrl,
documentUrl: document.URL,
requestType: requestType,
requestId: requestId
};
contentPage.sendMessage(message, onProcessShouldCollapseResponse);
};
/**
* Response callback for "processShouldCollapseMany" message.
*
* @param response Response from bg page.
*/
var onProcessShouldCollapseManyResponse = function (response) {
if (!response) {
return;
}
var requests = response.requests;
for (var i = 0; i < requests.length; i++) {
var collapseRequest = requests[i];
onProcessShouldCollapseResponse(collapseRequest);
}
};
/**
* Collects all elements from the page and checks if we should hide them.
*/
var checkBatchShouldCollapse = function () {
var requests = [];
// Collect collapse requests
for (var tagName in requestTypeMap) { // jshint ignore:line
var requestType = requestTypeMap[tagName];
var elements = document.getElementsByTagName(tagName);
for (var j = 0; j < elements.length; j++) {
var element = elements[j];
var elementUrl = getElementUrl(element);
if (!elementUrl) {
continue;
}
var requestId = saveCollapseRequest(element);
requests.push({
elementUrl: elementUrl,
requestType: requestType,
requestId: requestId,
tagName: tagName
});
}
}
var message = {
type: 'processShouldCollapseMany',
requests: requests,
documentUrl: document.URL
};
// Send all prepared requests in one message
contentPage.sendMessage(message, onProcessShouldCollapseManyResponse);
};
/**
* This method is used when we need to check all page elements with collapse rules.
* We need this when the browser is just started and add-on is not yet initialized.
* In this case content scripts waits for add-on initialization and the
* checks all page elements.
*/
var initBatchCollapse = function () {
if (document.readyState === 'complete' ||
document.readyState === 'loaded' ||
document.readyState === 'interactive') {
checkBatchShouldCollapse();
} else {
document.addEventListener('DOMContentLoaded', checkBatchShouldCollapse);
}
};
/**
* Called when document become visible.
* https://github.com/AdguardTeam/AdguardBrowserExtension/issues/159
*/
var onVisibilityChange = function () {
if (document.hidden === false) {
document.removeEventListener("visibilitychange", onVisibilityChange);
init();
}
};
/**
* Messaging won't work when page is loaded by Safari top hits
*/
if (contentPage.isSafari && document.hidden) {
document.addEventListener("visibilitychange", onVisibilityChange);
return;
}
// Start the content script
init();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment