uBlock/js/contentscript-end.js

490 lines
16 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
/******************************************************************************/
/******************************************************************************/
2014-07-02 10:02:29 -06:00
(function() {
/******************************************************************************/
2014-06-23 16:42:43 -06:00
// https://github.com/gorhill/httpswitchboard/issues/345
var messaging = (function(name){
var port = null;
var dangling = false;
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;
}
callback(details.msg);
delete requestIdToCallbackMap[details.id];
checkDisconnect();
};
var start = function(name) {
port = chrome.runtime.connect({
name: name +
'/' +
String.fromCharCode(
Math.random() * 0x7FFF | 0,
Math.random() * 0x7FFF | 0,
Math.random() * 0x7FFF | 0,
Math.random() * 0x7FFF | 0
)
});
port.onMessage.addListener(onPortMessage);
};
if ( typeof name === 'string' && name.length > 0 ) {
start(name);
}
var stop = function() {
listenCallback = null;
dangling = true;
checkDisconnect();
};
var ask = function(msg, callback) {
if ( !callback ) {
tell(msg);
return;
}
var id = requestId++;
port.postMessage({ id: id, msg: msg });
requestIdToCallbackMap[id] = callback;
};
var tell = function(msg) {
port.postMessage({ id: 0, msg: msg });
};
var listen = function(callback) {
listenCallback = callback;
};
var checkDisconnect = function() {
if ( !dangling ) {
return;
}
if ( Object.keys(requestIdToCallbackMap).length ) {
return;
}
port.disconnect();
port = null;
};
return {
start: start,
stop: stop,
ask: ask,
tell: tell,
listen: listen
};
})('contentscript-end.js');
/******************************************************************************/
/******************************************************************************/
// ABP cosmetic filters
(function() {
2014-07-02 10:02:29 -06:00
var queriedSelectors = {};
var injectedSelectors = {};
var classSelectors = null;
var idSelectors = null;
var domLoaded = function() {
// https://github.com/gorhill/uBlock/issues/14
// Treat any existing domain-specific exception selectors as if they had
// been injected already.
var style = document.getElementById('uBlock1ae7a5f130fc79b4fdb8a4272d9426b5');
var exceptions = style && style.getAttribute('uBlock1ae7a5f130fc79b4fdb8a4272d9426b5');
if ( exceptions ) {
exceptions = decodeURIComponent(exceptions).split('\n');
var i = exceptions.length;
while ( i-- ) {
injectedSelectors[exceptions[i]] = true;
}
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-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
}
2014-07-02 10:02:29 -06:00
if ( selectors.length > 0 ) {
//console.log('µBlock> ABP cosmetic filters: retrieving CSS rules using %d selectors', selectors.length);
messaging.ask({
what: 'retrieveGenericCosmeticSelectors',
pageURL: window.location.href,
selectors: selectors
},
retrieveHandler
);
}
idSelectors = null;
classSelectors = null;
};
2014-06-23 16:42:43 -06:00
2014-07-02 10:02:29 -06:00
var retrieveHandler = function(selectors) {
if ( !selectors ) {
return;
2014-06-23 16:42:43 -06:00
}
2014-07-02 10:02:29 -06:00
var styleText = [];
2014-07-04 14:47:34 -06:00
filterLowGenerics(selectors, 'hide');
filterHighGenerics(selectors, 'hide');
2014-07-02 10:02:29 -06:00
reduce(selectors.hide, injectedSelectors);
if ( selectors.hide.length ) {
var hideStyleText = '{{hideSelectors}} {display:none !important;}'
.replace('{{hideSelectors}}', selectors.hide.join(','));
styleText.push(hideStyleText);
applyCSS(selectors.hide, 'display', 'none');
//console.debug('µBlock> generic cosmetic filters: injecting %d CSS rules:', selectors.hide.length, hideStyleText);
2014-06-23 16:42:43 -06:00
}
2014-07-04 14:47:34 -06:00
filterLowGenerics(selectors, 'donthide');
filterHighGenerics(selectors, 'donthide');
2014-07-02 10:02:29 -06:00
reduce(selectors.donthide, injectedSelectors);
if ( selectors.donthide.length ) {
var dontHideStyleText = '{{donthideSelectors}} {display:initial !important;}'
.replace('{{donthideSelectors}}', selectors.donthide.join(','));
styleText.push(dontHideStyleText);
applyCSS(selectors.donthide, 'display', 'initial');
//console.debug('µBlock> generic cosmetic filters: injecting %d CSS rules:', selectors.donthide.length, dontHideStyleText);
}
if ( styleText.length > 0 ) {
var style = document.createElement('style');
style.appendChild(document.createTextNode(styleText.join('\n')));
var parent = document.body || document.documentElement;
if ( parent ) {
parent.appendChild(style);
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
var applyCSS = function(selectors, prop, value) {
if ( document.body === null ) {
return;
2014-06-23 16:42:43 -06:00
}
2014-07-02 10:02:29 -06:00
var elems = document.querySelectorAll(selectors);
var i = elems.length;
while ( i-- ) {
elems[i].style[prop] = value;
2014-06-23 16:42:43 -06:00
}
2014-07-02 10:02:29 -06:00
};
2014-07-04 14:47:34 -06:00
var filterTitleGeneric = function(generics, root, out) {
if ( !root.title.length ) {
return;
}
var selector = '[title="' + root.title + '"]';
if ( generics[selector] && !injectedSelectors[selector] ) {
out.push(selector);
}
selector = root.tagName + selector;
if ( generics[selector] && !injectedSelectors[selector] ) {
out.push(selector);
}
};
var filterAltGeneric = function(generics, root, out) {
var alt = root.getAttribute('alt');
if ( !alt || !alt.length ) {
return;
}
var selector = '[alt="' + root.title + '"]';
if ( generics[selector] && !injectedSelectors[selector] ) {
out.push(selector);
}
selector = root.tagName + selector;
if ( generics[selector] && !injectedSelectors[selector] ) {
out.push(selector);
}
};
var filterLowGenerics = function(selectors, what) {
if ( selectors[what + 'LowGenericCount'] === 0 ) {
return;
}
var out = selectors[what];
var generics = selectors[what + 'LowGenerics'];
var nodeList, iNode;
// Low generics: ["title"]
nodeList = document.querySelectorAll('[title]');
iNode = nodeList.length;
while ( iNode-- ) {
filterTitleGeneric(generics, nodeList[iNode], out);
}
// Low generics: ["alt"]
nodeList = document.querySelectorAll('[alt]');
iNode = nodeList.length;
while ( iNode-- ) {
filterAltGeneric(generics, nodeList[iNode], out);
}
};
var filterHighGenerics = function(selectors, what) {
var out = selectors[what];
var generics = selectors[what + 'HighGenerics'];
var iGeneric = generics.length;
2014-07-02 10:02:29 -06:00
var selector;
2014-07-04 14:47:34 -06:00
while ( iGeneric-- ) {
selector = generics[iGeneric];
2014-07-02 10:02:29 -06:00
if ( injectedSelectors[selector] ) {
2014-06-23 16:42:43 -06:00
continue;
}
2014-07-02 10:02:29 -06:00
if ( document.querySelector(selector) !== null ) {
2014-07-04 14:47:34 -06:00
out.push(selector);
2014-07-02 10:02:29 -06:00
}
}
};
var reduce = function(selectors, dict) {
var i = selectors.length, selector, end;
while ( i-- ) {
selector = selectors[i];
if ( !dict[selector] ) {
if ( end !== undefined ) {
selectors.splice(i+1, end-i);
end = undefined;
}
dict[selector] = true;
} else if ( end === undefined ) {
end = i;
}
}
if ( end !== undefined ) {
selectors.splice(0, end+1);
}
};
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;
// quite unlikely, so no need to be fancy
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
var processNodeLists = function(nodeLists) {
var i = nodeLists.length;
var nodeList, j, node;
while ( i-- ) {
nodeList = nodeLists[i];
2014-07-20 13:00:26 -06:00
idsFromNodeList(nodeList);
classesFromNodeList(nodeList);
2014-07-02 10:02:29 -06:00
j = nodeList.length;
while ( j-- ) {
node = nodeList[j];
2014-07-20 13:00:26 -06:00
if ( typeof node.querySelectorAll === 'function' ) {
idsFromNodeList(node.querySelectorAll('[id]'));
classesFromNodeList(node.querySelectorAll('[class]'));
2014-07-02 10:02:29 -06:00
}
}
2014-06-23 16:42:43 -06:00
}
2014-07-02 10:02:29 -06:00
retrieveGenericSelectors();
};
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 mutationObservedHandler = function(mutations) {
var iMutation = mutations.length;
var nodeLists = [], nodeList;
while ( iMutation-- ) {
nodeList = mutations[iMutation].addedNodes;
if ( nodeList && nodeList.length ) {
nodeLists.push(nodeList);
}
}
if ( nodeLists.length ) {
processNodeLists(nodeLists);
}
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-06-27 15:06:42 -06:00
// https://github.com/gorhill/uBlock/issues/7
2014-07-20 13:00:26 -06:00
(function() {
2014-07-02 10:02:29 -06:00
var hideOne = function(elem, collapse) {
// If `!important` is not there, going back using history will likely
// cause the hidden element to re-appear.
elem.style.visibility = 'hidden !important';
if ( collapse && elem.parentNode ) {
elem.parentNode.removeChild(elem);
}
2014-06-29 10:38:18 -06:00
};
2014-07-20 13:00:26 -06:00
// First pass
messaging.ask({ what: 'blockedRequests' }, function(details) {
var elems = document.querySelectorAll('img,iframe');
2014-07-02 10:02:29 -06:00
var blockedRequests = details.blockedRequests;
var collapse = details.collapse;
var i = elems.length;
var elem, src;
while ( i-- ) {
elem = elems[i];
src = elem.src;
2014-07-20 13:00:26 -06:00
if ( typeof src !== 'string' || src === '' ) {
2014-07-02 10:02:29 -06:00
continue;
}
2014-07-20 13:00:26 -06:00
if ( blockedRequests[src] ) {
2014-07-02 10:02:29 -06:00
hideOne(elem, collapse);
}
2014-06-27 15:06:42 -06:00
}
2014-07-20 13:00:26 -06:00
});
2014-07-02 10:02:29 -06:00
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 onResourceLoaded = function(ev) {
var target = ev.target;
2014-08-02 09:40:27 -06:00
if ( !target || !target.src ) { return; }
if ( target.tagName.toLowerCase() !== 'iframe' ) { return; }
var onAnswerReceived = function(details) {
if ( details.blocked ) {
hideOne(target, details.collapse);
}
};
messaging.ask({ what: 'blockedRequest', url: target.src }, onAnswerReceived);
};
var onResourceFailed = function(ev) {
var target = ev.target;
if ( !target || !target.src ) { return; }
if ( target.tagName.toLowerCase() !== 'img' ) { return; }
2014-07-20 13:00:26 -06:00
var onAnswerReceived = function(details) {
if ( details.blocked ) {
hideOne(target, details.collapse);
}
};
messaging.ask({ what: 'blockedRequest', url: target.src }, onAnswerReceived);
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
/******************************************************************************/
})();