Reinject styles on document-rewrite

uBlock-extra rewrites html of some sites known to have bad scripts. Such new documentElement doesn't have our styles so we need to detect this case by observing the parent document node non-recursively, meaning we don't add overhead to the normal browsing experience.
This commit is contained in:
tophf 2017-03-28 12:54:22 +03:00
parent e4c8ff9ff1
commit 486d4258d3

148
apply.js
View File

@ -3,15 +3,15 @@
/* eslint no-var: 0 */ /* eslint no-var: 0 */
'use strict'; 'use strict';
var isOwnPage = location.href.startsWith('chrome-extension:');
var disableAll = false; var disableAll = false;
var styleElements = new Map(); var styleElements = new Map();
var retiredStyleIds = []; var retiredStyleIds = [];
var iframeObserver; var iframeObserver;
var styleObserverSymbol = Symbol('Stylus.styleObserver'); var docRewriteObserver;
var orphanCheckTimer; var orphanCheckTimer;
initObserver(); initIFrameObserver();
initStyleObserver();
requestStyles(); requestStyles();
chrome.runtime.onMessage.addListener(applyOnMessage); chrome.runtime.onMessage.addListener(applyOnMessage);
@ -19,7 +19,7 @@ chrome.runtime.onMessage.addListener(applyOnMessage);
function requestStyles(options) { function requestStyles(options) {
// If this is a Stylish page (Edit Style or Manage Styles), // If this is a Stylish page (Edit Style or Manage Styles),
// we'll request the styles directly to minimize delay and flicker, // we'll request the styles directly to minimize delay and flicker,
// unless Chrome still starts up and the background page isn't fully loaded. // unless Chrome is still starting up and the background page isn't fully loaded.
// (Note: in this case the function may be invoked again from applyStyles.) // (Note: in this case the function may be invoked again from applyStyles.)
const request = Object.assign({ const request = Object.assign({
method: 'getStyles', method: 'getStyles',
@ -137,7 +137,7 @@ function applyStyleState(id, enabled, doc) {
function removeStyle(id, doc) { function removeStyle(id, doc) {
styleElements.delete('stylus-' + id); styleElements.delete('stylus-' + id);
removeStyleElements([doc.getElementById('stylus-' + id)]); [doc.getElementById('stylus-' + id)].forEach(e => e && e.remove());
if (doc == document && !styleElements.size) { if (doc == document && !styleElements.size) {
iframeObserver.disconnect(); iframeObserver.disconnect();
} }
@ -193,24 +193,7 @@ function applyStyles(styleHash) {
} else { } else {
document.addEventListener('DOMContentLoaded', onDOMContentLoaded); document.addEventListener('DOMContentLoaded', onDOMContentLoaded);
} }
initDocRewriteObserver();
if (!location.href.startsWith('chrome-extension:')) {
const t0 = performance.now();
let counter = 0;
console.warn(location.href, 'START');
const interval = setInterval(() => {
counter++;
for (const [id, el] of styleElements.entries()) {
if (!document.getElementById(id)) {
document.documentElement.appendChild(el);
console.log(location.href, el);
} else if (performance.now() - t0 > 1000) {
console.warn(location.href, 'watchdog fired', counter, 'times');
clearInterval(interval);
}
}
}, 10);
}
} }
if (retiredStyleIds.length) { if (retiredStyleIds.length) {
@ -277,7 +260,7 @@ function addDocumentStylesToIFrame(iframe) {
addStyleElement(el, doc); addStyleElement(el, doc);
} }
} }
initStyleObserver(doc); initDocRewriteObserver(doc);
} }
@ -285,6 +268,7 @@ function addDocumentStylesToAllIFrames() {
getDynamicIFrames(document).forEach(addDocumentStylesToIFrame); getDynamicIFrames(document).forEach(addDocumentStylesToIFrame);
} }
// Only dynamic iframes get the parent document's styles. Other ones should get styles based on their own URLs. // Only dynamic iframes get the parent document's styles. Other ones should get styles based on their own URLs.
function getDynamicIFrames(doc) { function getDynamicIFrames(doc) {
return [...doc.getElementsByTagName('iframe')].filter(iframeIsDynamic); return [...doc.getElementsByTagName('iframe')].filter(iframeIsDynamic);
@ -346,42 +330,23 @@ function replaceAll(newStyles, doc) {
function replaceAllpass2(newStyles, doc) { function replaceAllpass2(newStyles, doc) {
const oldStyles = [...doc.querySelectorAll('STYLE.stylus[id$="-ghost"]')]; const oldStyles = [...doc.querySelectorAll('STYLE.stylus[id$="-ghost"]')];
processDynamicIFrames(doc, replaceAllpass2, newStyles); processDynamicIFrames(doc, replaceAllpass2, newStyles);
removeStyleElements(oldStyles); oldStyles.forEach(e => e.remove);
} }
function removeStyleElements(elements) { function initIFrameObserver() {
if (!elements[0]) {
return;
}
const styleObserver = elements[0].ownerDocument[styleObserverSymbol];
if (styleObserver) {
styleObserver.disconnect();
}
for (const el of elements) {
el.remove();
}
if (styleObserver) {
styleObserver.start();
}
}
// Observe dynamic IFRAMEs being added
function initObserver() {
const iframesCollection = document.getElementsByTagName('iframe');
iframeObserver = Object.assign(new MutationObserver(observer), { iframeObserver = Object.assign(new MutationObserver(observer), {
start() { start() {
this.observe(document, {childList: true, subtree: true}); this.observe(document, {childList: true, subtree: true});
} }
}); });
const iframesCollection = document.getElementsByTagName('iframe');
function observer(mutations) { function observer(mutations) {
// MutationObserver runs as a microtask so the timer won't fire if (!isOwnPage) {
// until all queued mutations are fired clearTimeout(orphanCheckTimer);
clearTimeout(orphanCheckTimer); orphanCheckTimer = setTimeout(orphanCheck, 1000);
orphanCheckTimer = setTimeout(orphanCheck, 0); }
// autoupdated HTMLCollection is superfast // autoupdated HTMLCollection is superfast
if (!iframesCollection[0]) { if (!iframesCollection[0]) {
return; return;
@ -419,50 +384,28 @@ function initObserver() {
} }
function initStyleObserver(doc = document) { function initDocRewriteObserver() {
const observer = Object.assign(new MutationObserver(styleObserver), { if (isOwnPage) {
counters: new Map(), return;
start() { }
this.observe(doc.documentElement, {childList: true}); // re-add styles if we detect documentElement being recreated
} docRewriteObserver = new MutationObserver(observer);
}); docRewriteObserver.observe(document, {childList: true});
doc[styleObserverSymbol] = observer;
observer.start();
}
function observer(mutations) {
function styleObserver(mutations, observer) { for (const mutation of mutations) {
//console.log(location.href, for (const node of mutation.addedNodes) {
// [].concat.apply([], mutations.map(m => [...m.addedNodes])), if (node.localName != 'html') {
// [].concat.apply([], mutations.map(m => [...m.removedNodes])) continue;
//); }
for (var m = 0, mutation; (mutation = mutations[m++]);) { for (const [id, el] of styleElements.entries()) {
var removed = mutation.removedNodes; if (!document.getElementById(id)) {
for (var n = 0, node; (node = removed[n++]);) { document.documentElement.appendChild(el);
let id = node.id;
var ourElement = styleElements.get(id);
if (!ourElement) {
for (const [elId, el] of styleElements.entries()) {
if (el == node) {
node.id = id = elId;
ourElement = el;
break;
} }
} }
document.addEventListener('DOMContentLoaded', onDOMContentLoaded);
return;
} }
if (!ourElement) {
continue;
}
const counter = observer.counters.get(id) || 0;
if (counter > 10) {
continue;
}
observer.counters.set(id, counter + 1);
if (ourElement.ownerDocument != node.ownerDocument) {
ourElement = node.ownerDocument.importNode(ourElement, true);
}
node.ownerDocument.documentElement.appendChild(ourElement);
//console.log('Restoring style', ourElement);
} }
} }
} }
@ -481,18 +424,10 @@ function orphanCheck() {
iframeObserver.takeRecords(); iframeObserver.takeRecords();
iframeObserver.disconnect(); iframeObserver.disconnect();
iframeObserver = null; iframeObserver = null;
document[styleObserverSymbol].disconnect(); if (docRewriteObserver) {
document[styleObserverSymbol] = null; docRewriteObserver.disconnect();
(function removeStyleObservers(doc) { docRewriteObserver = null;
getDynamicIFrames(doc).forEach(iframe => { }
const styleObserver = iframe.contentDocument[styleObserverSymbol];
if (styleObserver) {
styleObserver.disconnect();
document[styleObserverSymbol] = null;
}
removeStyleObservers(iframe.contentDocument);
});
})(document);
// we can detach event listeners // we can detach event listeners
document.removeEventListener('DOMContentLoaded', onDOMContentLoaded); document.removeEventListener('DOMContentLoaded', onDOMContentLoaded);
// we can't detach chrome.runtime.onMessage because it's no longer connected internally // we can't detach chrome.runtime.onMessage because it's no longer connected internally
@ -507,12 +442,12 @@ function orphanCheck() {
'applyStyles', 'applyStyles',
'doDisableAll', 'doDisableAll',
'getDynamicIFrames', 'getDynamicIFrames',
'processDynamicIFrames',
'iframeIsDynamic', 'iframeIsDynamic',
'iframeIsLoadingSrcDoc', 'iframeIsLoadingSrcDoc',
'initObserver', 'initDocRewriteObserver',
'initStyleObserver', 'initIFrameObserver',
'orphanCheck', 'orphanCheck',
'processDynamicIFrames',
'removeStyle', 'removeStyle',
'replaceAll', 'replaceAll',
'replaceAllpass2', 'replaceAllpass2',
@ -520,8 +455,9 @@ function orphanCheck() {
'retireStyle', 'retireStyle',
'styleObserver', 'styleObserver',
// variables // variables
'styleElements', 'docRewriteObserver',
'iframeObserver', 'iframeObserver',
'retiredStyleIds', 'retiredStyleIds',
'styleElements',
].forEach(fn => (window[fn] = null)); ].forEach(fn => (window[fn] = null));
} }