uBlock/js/contentscript-end.js

698 lines
23 KiB
JavaScript
Raw Normal View History

2014-06-23 16:42:43 -06:00
/*******************************************************************************
µBlock - a Chromium browser extension to block requests.
Copyright (C) 2014 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/uBlock
*/
/* global chrome */
// Injected into content pages
/******************************************************************************/
/******************************************************************************/
// https://github.com/gorhill/httpswitchboard/issues/345
var uBlockMessaging = (function(name){
2014-06-23 16:42:43 -06:00
var port = null;
var requestId = 1;
var requestIdToCallbackMap = {};
var listenCallback = null;
var onPortMessage = function(details) {
if ( typeof details.id !== 'number' ) {
return;
}
// Announcement?
if ( details.id < 0 ) {
if ( listenCallback ) {
listenCallback(details.msg);
}
return;
}
var callback = requestIdToCallbackMap[details.id];
if ( !callback ) {
return;
}
2014-08-28 09:08:51 -06:00
// Must be removed before calling client to be sure to not execute
// callback again if the client stops the messaging service.
2014-06-23 16:42:43 -06:00
delete requestIdToCallbackMap[details.id];
2014-08-28 09:08:51 -06:00
callback(details.msg);
2014-06-23 16:42:43 -06:00
};
var start = function(name) {
2014-09-14 14:20:40 -06:00
port = chrome.runtime.connect({ name: name });
2014-06-23 16:42:43 -06:00
port.onMessage.addListener(onPortMessage);
2014-08-28 09:08:51 -06:00
// https://github.com/gorhill/uBlock/issues/193
port.onDisconnect.addListener(stop);
};
2014-06-23 16:42:43 -06:00
var stop = function() {
listenCallback = null;
2014-08-28 09:08:51 -06:00
port.disconnect();
port = null;
flushCallbacks();
2014-06-23 16:42:43 -06:00
};
2014-08-28 09:08:51 -06:00
if ( typeof name === 'string' && name !== '' ) {
start(name);
}
2014-06-23 16:42:43 -06:00
var ask = function(msg, callback) {
2014-08-28 09:08:51 -06:00
if ( port === null ) {
if ( typeof callback === 'function' ) {
callback();
}
return;
}
if ( callback === undefined ) {
2014-06-23 16:42:43 -06:00
tell(msg);
return;
}
var id = requestId++;
port.postMessage({ id: id, msg: msg });
requestIdToCallbackMap[id] = callback;
};
var tell = function(msg) {
2014-08-28 09:08:51 -06:00
if ( port !== null ) {
port.postMessage({ id: 0, msg: msg });
}
2014-06-23 16:42:43 -06:00
};
var listen = function(callback) {
listenCallback = callback;
};
2014-08-28 09:08:51 -06:00
var flushCallbacks = function() {
var callback;
2014-09-14 14:20:40 -06:00
for ( var id in requestIdToCallbackMap ) {
2014-08-28 09:08:51 -06:00
if ( requestIdToCallbackMap.hasOwnProperty(id) === false ) {
continue;
}
callback = requestIdToCallbackMap[id];
if ( !callback ) {
continue;
}
// Must be removed before calling client to be sure to not execute
// callback again if the client stops the messaging service.
delete requestIdToCallbackMap[id];
callback();
2014-06-23 16:42:43 -06:00
}
};
return {
start: start,
stop: stop,
ask: ask,
tell: tell,
listen: listen
};
})('contentscript-end.js');
/******************************************************************************/
/******************************************************************************/
// ABP cosmetic filters
(function() {
var messaging = uBlockMessaging;
2014-07-02 10:02:29 -06:00
var queriedSelectors = {};
var injectedSelectors = {};
var classSelectors = null;
var idSelectors = null;
var highGenerics = null;
var contextNodes = [document];
var nullArray = { push: function(){} };
2014-07-02 10:02:29 -06:00
var domLoaded = function() {
var style = document.getElementById('ublock-preload-1ae7a5f130fc79b4fdb8a4272d9426b5');
2014-08-13 18:03:55 -06:00
if ( style ) {
// https://github.com/gorhill/uBlock/issues/14
// Treat any existing domain-specific exception selectors as if
// they had been injected already.
var selectors, i;
var exceptions = style.getAttribute('data-ublock-exceptions');
2014-08-13 18:03:55 -06:00
if ( exceptions ) {
selectors = JSON.parse(exceptions);
i = selectors.length;
while ( i-- ) {
injectedSelectors[selectors[i]] = true;
}
}
// Avoid re-injecting already injected CSS rules.
selectors = selectorsFromStyles(style);
i = selectors.length;
2014-07-02 10:02:29 -06:00
while ( i-- ) {
2014-08-13 18:03:55 -06:00
injectedSelectors[selectors[i]] = true;
2014-07-02 10:02:29 -06:00
}
2014-08-17 14:07:42 -06:00
// https://github.com/gorhill/uBlock/issues/158
// Ensure injected styles are enforced
hideElements(selectors.join(','));
2014-06-25 16:44:35 -06:00
}
2014-07-20 13:00:26 -06:00
idsFromNodeList(document.querySelectorAll('[id]'));
classesFromNodeList(document.querySelectorAll('[class]'));
2014-07-02 10:02:29 -06:00
retrieveGenericSelectors();
};
2014-06-23 16:42:43 -06:00
2014-08-13 18:03:55 -06:00
var selectorsFromStyles = function(styleRef) {
var selectors = [];
var styles = typeof styleRef === 'string' ?
document.querySelectorAll(styleRef):
[styleRef];
var i = styles.length;
var style, subset, lastSelector, pos;
while ( i-- ) {
style = styles[i];
subset = style.textContent.split(',\n');
lastSelector = subset.pop();
if ( lastSelector ) {
pos = lastSelector.indexOf('\n');
if ( pos !== -1 ) {
subset.push(lastSelector.slice(0, pos));
}
}
selectors = selectors.concat(subset);
}
return selectors;
};
2014-07-02 10:02:29 -06:00
var retrieveGenericSelectors = function() {
var selectors = classSelectors !== null ? Object.keys(classSelectors) : [];
if ( idSelectors !== null ) {
selectors = selectors.concat(idSelectors);
2014-06-23 16:42:43 -06:00
}
if ( selectors.length > 0 || highGenerics === null ) {
2014-07-02 10:02:29 -06:00
//console.log('µBlock> ABP cosmetic filters: retrieving CSS rules using %d selectors', selectors.length);
messaging.ask({
what: 'retrieveGenericCosmeticSelectors',
pageURL: window.location.href,
selectors: selectors,
highGenerics: highGenerics === null
2014-07-02 10:02:29 -06:00
},
retrieveHandler
);
} else {
retrieveHandler(null);
2014-07-02 10:02:29 -06:00
}
idSelectors = null;
classSelectors = null;
};
2014-06-23 16:42:43 -06:00
2014-07-02 10:02:29 -06:00
var retrieveHandler = function(selectors) {
//console.debug('µBlock> contextNodes = %o', contextNodes);
if ( selectors && selectors.highGenerics ) {
highGenerics = selectors.highGenerics;
2014-06-23 16:42:43 -06:00
}
if ( selectors && selectors.donthide.length ) {
processLowGenerics(selectors.donthide, nullArray);
}
if ( highGenerics ) {
if ( highGenerics.donthideLowCount ) {
processHighLowGenerics(highGenerics.donthideLow, nullArray);
}
if ( highGenerics.donthideMediumCount ) {
processHighMediumGenerics(highGenerics.donthideMedium, nullArray);
2014-08-07 14:12:15 -06:00
}
}
// No such thing as high-high generic exceptions.
//if ( highGenerics.donthideHighCount ) {
// processHighHighGenerics(document, highGenerics.donthideHigh, nullArray);
//}
var hideSelectors = [];
if ( selectors && selectors.hide.length ) {
processLowGenerics(selectors.hide, hideSelectors);
}
if ( highGenerics ) {
if ( highGenerics.hideLowCount ) {
processHighLowGenerics(highGenerics.hideLow, hideSelectors);
}
if ( highGenerics.hideMediumCount ) {
processHighMediumGenerics(highGenerics.hideMedium, hideSelectors);
}
if ( highGenerics.hideHighCount ) {
processHighHighGenerics(highGenerics.hideHigh, hideSelectors);
}
}
if ( hideSelectors.length ) {
2014-08-17 14:07:42 -06:00
hideElements(hideSelectors);
2014-07-02 10:02:29 -06:00
var style = document.createElement('style');
style.setAttribute('class', 'ublock-postload-1ae7a5f130fc79b4fdb8a4272d9426b5');
2014-08-13 18:03:55 -06:00
// The linefeed before the style block is very important: do no remove!
style.appendChild(document.createTextNode(hideSelectors.join(',\n') + '\n{display:none !important;}'));
2014-07-02 10:02:29 -06:00
var parent = document.body || document.documentElement;
if ( parent ) {
parent.appendChild(style);
2014-06-23 16:42:43 -06:00
}
2014-08-13 18:03:55 -06:00
messaging.tell({
what: 'injectedSelectors',
type: 'cosmetic',
hostname: window.location.hostname,
selectors: hideSelectors
2014-08-13 18:03:55 -06:00
});
//console.debug('µBlock> generic cosmetic filters: injecting %d CSS rules:', hideSelectors.length, text);
2014-06-23 16:42:43 -06:00
}
contextNodes.length = 0;
2014-07-02 10:02:29 -06:00
};
2014-06-23 16:42:43 -06:00
2014-08-17 14:07:42 -06:00
var hideElements = function(selectors) {
2014-09-03 18:14:55 -06:00
// https://github.com/gorhill/uBlock/issues/207
// Do not call querySelectorAll() using invalid CSS selectors
if ( selectors.length === 0 ) {
return;
}
2014-07-02 10:02:29 -06:00
if ( document.body === null ) {
return;
2014-06-23 16:42:43 -06:00
}
2014-08-17 14:07:42 -06:00
// https://github.com/gorhill/uBlock/issues/158
// Using CSSStyleDeclaration.setProperty is more reliable
2014-07-02 10:02:29 -06:00
var elems = document.querySelectorAll(selectors);
var i = elems.length;
while ( i-- ) {
2014-08-17 14:07:42 -06:00
elems[i].style.setProperty('display', 'none', 'important');
2014-06-23 16:42:43 -06:00
}
2014-07-02 10:02:29 -06:00
};
var selectNodes = function(selector) {
var targetNodes = [];
var i = contextNodes.length;
var node, nodeList, j;
var doc = document;
while ( i-- ) {
node = contextNodes[i];
if ( node === doc ) {
return doc.querySelectorAll(selector);
}
targetNodes.push(node);
nodeList = node.querySelectorAll(selector);
j = nodeList.length;
while ( j-- ) {
targetNodes.push(nodeList[j]);
}
2014-07-04 14:47:34 -06:00
}
return targetNodes;
2014-07-04 14:47:34 -06:00
};
var processLowGenerics = function(generics, out) {
var i = generics.length;
var selector;
while ( i-- ) {
selector = generics[i];
if ( injectedSelectors[selector] !== undefined ) {
continue;
}
injectedSelectors[selector] = true;
out.push(selector);
2014-07-04 14:47:34 -06:00
}
};
var processHighLowGenerics = function(generics, out) {
var attrs = ['title', 'alt'];
2014-08-13 18:03:55 -06:00
var attr, attrValue, nodeList, iNode, node;
var selector;
while ( attr = attrs.pop() ) {
nodeList = selectNodes('[' + attr + ']');
iNode = nodeList.length;
while ( iNode-- ) {
node = nodeList[iNode];
attrValue = node.getAttribute(attr);
if ( !attrValue ) { continue; }
selector = '[' + attr + '="' + attrValue + '"]';
2014-08-13 18:03:55 -06:00
if ( generics[selector] ) {
if ( injectedSelectors[selector] === undefined ) {
injectedSelectors[selector] = true;
out.push(selector);
}
}
selector = node.tagName.toLowerCase() + selector;
2014-08-13 18:03:55 -06:00
if ( generics[selector] ) {
if ( injectedSelectors[selector] === undefined ) {
injectedSelectors[selector] = true;
out.push(selector);
}
}
}
2014-07-04 14:47:34 -06:00
}
};
var processHighMediumGenerics = function(generics, out) {
var nodeList = selectNodes('a[href^="http"]');
var iNode = nodeList.length;
2014-08-13 18:03:55 -06:00
var node, href, pos, hash, selectors, selector, iSelector;
while ( iNode-- ) {
node = nodeList[iNode];
href = node.getAttribute('href');
if ( !href ) { continue; }
pos = href.indexOf('://');
if ( pos === -1 ) { continue; }
hash = href.slice(pos + 3, pos + 11);
2014-08-13 18:03:55 -06:00
selectors = generics[hash];
if ( selectors === undefined ) { continue; }
selectors = selectors.split(',\n');
iSelector = selectors.length;
while ( iSelector-- ) {
selector = selectors[iSelector];
if ( injectedSelectors[selector] === undefined ) {
injectedSelectors[selector] = true;
out.push(selector);
2014-08-13 18:03:55 -06:00
}
2014-07-02 10:02:29 -06:00
}
}
};
var processHighHighGenerics = function(generics, out) {
2014-08-13 18:03:55 -06:00
if ( injectedSelectors['{{highHighGenerics}}'] !== undefined ) { return; }
2014-08-12 18:25:11 -06:00
if ( document.querySelector(generics) === null ) { return; }
2014-08-13 18:03:55 -06:00
injectedSelectors['{{highHighGenerics}}'] = true;
var selectors = generics.split(',\n');
var iSelector = selectors.length;
var selector;
while ( iSelector-- ) {
selector = selectors[iSelector];
if ( injectedSelectors[selector] === undefined ) {
injectedSelectors[selector] = true;
out.push(selector);
2014-07-02 10:02:29 -06:00
}
}
};
2014-07-20 13:00:26 -06:00
var idsFromNodeList = function(nodes) {
2014-07-02 10:02:29 -06:00
if ( !nodes || !nodes.length ) {
return;
2014-06-23 16:42:43 -06:00
}
2014-07-02 10:02:29 -06:00
if ( idSelectors === null ) {
idSelectors = [];
}
var qq = queriedSelectors;
var ii = idSelectors;
2014-07-20 13:00:26 -06:00
var node, v;
2014-07-02 10:02:29 -06:00
var i = nodes.length;
while ( i-- ) {
node = nodes[i];
2014-07-20 13:00:26 -06:00
if ( node.nodeType !== 1 ) { continue; }
2014-07-02 10:02:29 -06:00
// id
2014-07-08 23:53:49 -06:00
v = nodes[i].id;
2014-07-20 13:00:26 -06:00
if ( typeof v !== 'string' ) { continue; }
2014-07-08 23:53:49 -06:00
v = v.trim();
2014-07-20 13:00:26 -06:00
if ( v === '' ) { continue; }
v = '#' + v;
if ( qq[v] ) { continue; }
ii.push(v);
qq[v] = true;
}
};
var classesFromNodeList = function(nodes) {
if ( !nodes || !nodes.length ) {
return;
}
if ( classSelectors === null ) {
classSelectors = {};
}
var qq = queriedSelectors;
var cc = classSelectors;
var node, v, vv, j;
var i = nodes.length;
while ( i-- ) {
node = nodes[i];
if ( node.nodeType !== 1 ) { continue; }
2014-07-02 10:02:29 -06:00
// class
v = nodes[i].className;
// it could be an SVGAnimatedString...
if ( typeof v !== 'string' ) { continue; }
v = v.trim();
if ( v === '' ) { continue; }
// one class
if ( v.indexOf(' ') < 0 ) {
v = '.' + v;
if ( qq[v] ) { continue; }
cc[v] = true;
qq[v] = true;
2014-06-23 16:42:43 -06:00
continue;
}
2014-07-02 10:02:29 -06:00
// many classes
2014-07-20 13:00:26 -06:00
vv = v.trim().split(' ');
j = vv.length;
2014-07-02 10:02:29 -06:00
while ( j-- ) {
2014-07-20 13:00:26 -06:00
v = vv[j].trim();
2014-07-02 10:02:29 -06:00
if ( v === '' ) { continue; }
v = '.' + v;
if ( qq[v] ) { continue; }
cc[v] = true;
qq[v] = true;
}
2014-06-23 16:42:43 -06:00
}
2014-07-02 10:02:29 -06:00
};
2014-06-23 16:42:43 -06:00
2014-07-02 10:02:29 -06:00
domLoaded();
2014-06-23 16:42:43 -06:00
// Observe changes in the DOM only if...
// - there is a document.body
// - there is at least one `script` tag
if ( !document.body || !document.querySelector('script') ) {
return;
}
var ignoreTags = {
'link': true,
'LINK': true,
'script': true,
'SCRIPT': true,
'style': true,
'STYLE': true
};
var mutationObservedHandler = function(mutations) {
var iMutation = mutations.length;
var nodeList, iNode, node;
while ( iMutation-- ) {
nodeList = mutations[iMutation].addedNodes;
if ( !nodeList ) {
continue;
}
iNode = nodeList.length;
while ( iNode-- ) {
node = nodeList[iNode];
if ( typeof node.querySelectorAll !== 'function' ) {
continue;
}
2014-09-05 14:13:48 -06:00
if ( ignoreTags.hasOwnProperty(node.tagName) ) {
continue;
}
contextNodes.push(node);
}
}
if ( contextNodes.length !== 0 ) {
idsFromNodeList(selectNodes('[id]'));
classesFromNodeList(selectNodes('[class]'));
retrieveGenericSelectors();
}
2014-07-02 10:02:29 -06:00
};
// https://github.com/gorhill/httpswitchboard/issues/176
var observer = new MutationObserver(mutationObservedHandler);
observer.observe(document.body, {
attributes: false,
childList: true,
characterData: false,
subtree: true
});
2014-07-02 10:02:29 -06:00
})();
2014-06-23 16:42:43 -06:00
2014-08-02 09:40:27 -06:00
/******************************************************************************/
2014-06-23 16:42:43 -06:00
/******************************************************************************/
2014-09-14 14:20:40 -06:00
// Permanent
2014-06-27 15:06:42 -06:00
2014-07-20 13:00:26 -06:00
(function() {
var messaging = uBlockMessaging;
2014-07-20 13:00:26 -06:00
// Listeners to mop up whatever is otherwise missed:
// - Future requests not blocked yet
// - Elements dynamically added to the page
// - Elements which resource URL changes
var loadedElements = {
'iframe': 'src'
};
var failedElements = {
'img': 'src',
2014-09-05 14:13:48 -06:00
'input': 'src',
'object': 'data'
};
var onResource = function(target, dict) {
if ( !target ) {
return;
}
var tagName = target.tagName.toLowerCase();
var prop = dict[tagName];
if ( prop === undefined ) {
return;
}
var src = target[prop];
if ( typeof src !== 'string' || src === '' ) {
return;
}
2014-09-14 14:20:40 -06:00
if ( src.slice(0, 4) !== 'http' ) {
return;
}
2014-08-25 06:49:08 -06:00
// https://github.com/gorhill/uBlock/issues/174
// Do not remove fragment from src URL
2014-08-02 09:40:27 -06:00
var onAnswerReceived = function(details) {
2014-09-14 14:20:40 -06:00
if ( typeof details !== 'object' || details === null ) {
return;
2014-08-02 09:40:27 -06:00
}
// If `!important` is not there, going back using history will
// likely cause the hidden element to re-appear.
2014-08-15 08:40:25 -06:00
if ( details.collapse ) {
if ( target.parentNode ) {
target.parentNode.removeChild(target);
} else {
2014-09-05 14:13:48 -06:00
target.style.setProperty('display', 'none', 'important');
2014-08-15 08:40:25 -06:00
}
2014-09-05 14:13:48 -06:00
} else {
target.style.setProperty('visibility', 'hidden', 'important');
}
messaging.tell({
what: 'injectedSelectors',
type: 'net',
hostname: window.location.hostname,
selectors: tagName + '[' + prop + '="' + src + '"]'
});
2014-08-02 09:40:27 -06:00
};
2014-09-14 14:20:40 -06:00
var details = {
what: 'filterRequest',
tagName: tagName,
requestURL: src,
pageHostname: window.location.hostname,
pageURL: window.location.href
};
messaging.ask(details, onAnswerReceived);
};
var onResourceLoaded = function(ev) {
2014-08-15 08:34:13 -06:00
//console.debug('Loaded %s[src="%s"]', ev.target.tagName, ev.target.src);
onResource(ev.target, loadedElements);
2014-08-02 09:40:27 -06:00
};
2014-08-02 09:40:27 -06:00
var onResourceFailed = function(ev) {
2014-09-05 14:13:48 -06:00
//console.debug('Failed to load %s[src="%s"]', ev.target.tagName, ev.target.src);
onResource(ev.target, failedElements);
2014-06-27 15:06:42 -06:00
};
2014-07-20 13:00:26 -06:00
document.addEventListener('load', onResourceLoaded, true);
2014-08-02 09:40:27 -06:00
document.addEventListener('error', onResourceFailed, true);
2014-07-02 10:02:29 -06:00
})();
2014-06-23 16:42:43 -06:00
/******************************************************************************/
2014-09-14 14:20:40 -06:00
/******************************************************************************/
// https://github.com/gorhill/uBlock/issues/7
// Executed only once
(function() {
var messaging = uBlockMessaging;
var srcProps = {
'embed': 'src',
'iframe': 'src',
'img': 'src',
'object': 'data'
};
var elements = [];
var onAnswerReceived = function(details) {
if ( typeof details !== 'object' || details === null ) {
return;
}
var requests = details.requests;
var collapse = details.collapse;
var selectors = [];
var i = requests.length;
var request, elem;
while ( i-- ) {
request = requests[i];
elem = elements[request.index];
if ( collapse ) {
if ( elem.parentNode ) {
elem.parentNode.removeChild(elem);
} else {
elem.style.setProperty('display', 'none', 'important');
}
} else {
elem.style.setProperty('visibility', 'hidden', 'important');
}
selectors.push(request.tagName + '[' + srcProps[request.tagName] + '="' + request.url + '"]');
}
if ( selectors.length !== 0 ) {
messaging.tell({
what: 'injectedSelectors',
type: 'net',
hostname: window.location.hostname,
selectors: selectors
});
}
};
var requests = [];
var tagNames = ['embed','iframe','img','object'];
var elementIndex = 0;
var tagName, elems, i, elem, prop, src;
while ( tagName = tagNames.pop() ) {
elems = document.getElementsByTagName(tagName);
i = elems.length;
while ( i-- ) {
elem = elems[i];
prop = srcProps[tagName];
if ( prop === undefined ) {
continue;
}
src = elem[prop];
if ( typeof src !== 'string' || src === '' ) {
continue;
}
if ( src.slice(0, 4) !== 'http' ) {
continue;
}
requests.push({
index: elementIndex,
tagName: tagName,
url: src
});
elements[elementIndex] = elem;
elementIndex += 1;
}
}
var details = {
what: 'filterRequests',
pageURL: window.location.href,
pageHostname: window.location.hostname,
requests: requests
};
messaging.ask(details, onAnswerReceived);
})();
/******************************************************************************/