Skip to content

Commit

Permalink
Fix style injection in pages + optimizations (#67)
Browse files Browse the repository at this point in the history
* All cosmetics are now using only one background action (instead of two)
* No unloading is needed in content-script anymore
* Simplified and optimized the implementation of CosmeticBucket
* Internalized the version of serialized engine for auto-invalidation on update
  • Loading branch information
remusao authored Dec 4, 2018
1 parent d9c8d6f commit 3f68596
Show file tree
Hide file tree
Showing 20 changed files with 201 additions and 568 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

*not released yet*

* Fix style injection and cosmetic filtering logic [#67](https://github.com/cliqz-oss/adblocker/pull/67)
* All cosmetics are now using only one background action (instead of two)
* No unloading is needed in content-script anymore
* Simplified and optimized the implementation of CosmeticBucket
* Internalized the version of serialized engine for auto-invalidation on update
* Fix cosmetic matching (tokenization bug) [#65](https://github.com/cliqz-oss/adblocker/pull/65)
* Optimize serialization and properly handle unicode in filters [#61](https://github.com/cliqz-oss/adblocker/pull/61)

Expand Down
7 changes: 2 additions & 5 deletions example/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ function loadAdblocker() {
loadCosmeticFilters: true,
loadNetworkFilters: true,
optimizeAOT: true,
version: 1,
});

console.log('Fetching resources...');
Expand Down Expand Up @@ -143,10 +142,8 @@ loadAdblocker().then((engine) => {
}

// Answer to content-script with a list of nodes
if (msg.action === 'getCosmeticsForDomain') {
sendResponse(engine.getDomainFilters(hostname));
} else if (msg.action === 'getCosmeticsForNodes') {
sendResponse(engine.getCosmeticsFilters(hostname, msg.args[0]));
if (msg.action === 'getCosmeticsFilters') {
sendResponse(engine.getCosmeticsFilters(hostname));
}
});

Expand Down
14 changes: 0 additions & 14 deletions example/content-script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,3 @@ const backgroundAction = (action, ...args): Promise<void> => {
const injection = new CosmeticsInjection(window, backgroundAction);

injection.injectCircumvention();

/**
* Make sure we clean-up all resources and event listeners when this content
* script is unloaded (stop MutationObserver, etc.).
*/
const onUnload = () => {
injection.unload();
window.removeEventListener('unload', onUnload);
};

/**
* Make sure we clean-up when content script is unloaded.
*/
window.addEventListener('unload', onUnload);
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"rollup-plugin-node-resolve": "^3.4.0",
"ts-jest": "^23.10.4",
"tslint": "^5.11.0",
"typescript": "^3.1.6"
"typescript": "^3.2.1"
},
"dependencies": {
"punycode": "^2.1.1",
Expand Down
2 changes: 2 additions & 0 deletions src/content/circumvention/generic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const swallowOwnErrors = bundle((magic) => {
}

if (windowOnError instanceof Function) {
// @ts-ignore
return windowOnError.apply(this, arguments);
}

Expand All @@ -47,6 +48,7 @@ export const protectConsole = bundle(() => {
return;
}
}
// @ts-ignore
return originalLog.apply(originalConsole, arguments);
}.bind(console),
});
Expand Down
10 changes: 5 additions & 5 deletions src/content/injection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,13 @@ export function blockScript(filter: string, doc: Document): void {
}

export function injectCSSRule(rule: string, doc: Document): void {
const css = doc.createElement('style');
css.type = 'text/css';
css.id = 'cliqz-adblokcer-css-rules';
const parent = doc.head || doc.documentElement;
const parent = doc.head || doc.getElementsByTagName('head')[0] || doc.documentElement;
if (parent !== null) {
parent.appendChild(css);
const css = doc.createElement('style');
css.type = 'text/css';
css.id = 'cliqz-adblokcer-css-rules';
css.appendChild(doc.createTextNode(rule));
parent.appendChild(css);
}
}

Expand Down
197 changes: 9 additions & 188 deletions src/cosmetics-injection.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
import injectCircumvention from './content/circumvention';
import { blockScript, injectCSSRule, injectScript } from './content/injection';

// We need this as `MutationObserver` is currently not part of the `Window` type
// provided by typescript, although it should be! This will be erased at compile
// time so it has no impact on produced code.
declare global {
interface Window {
MutationObserver?: typeof MutationObserver;
}
}

interface IMessageFromBackground {
active: boolean;
scripts: string[];
Expand All @@ -22,65 +13,18 @@ interface IMessageFromBackground {
* - Inject scripts.
* - Block scripts.
* - Inject CSS rules.
* - Monitor changes using a mutation observer and inject new rules if needed.
*
* All this happens by communicating with the background through the
* `backgroundAction` function (to trigger request the sending of new rules
* based on a domain or node selectors) and the `handleResponseFromBackground`
* callback to apply new rules.
*/
export default class CosmeticInjection {
private window: Window;

// TODO: split into two callbacks:
// 1. getCosmeticsForDomain
// 2. getCosmeticsForNodes
// Each of them could return a promise resolving to the filters to be injected
// in the page, if any. Currently the communication is async, but a
// promise-based API would be nicer to use.
private backgroundAction: (action: string, ...args: any[]) => Promise<void>;
private injectedRules: Set<string>;
private injectedScripts: Set<string>;
private blockedScripts: Set<string>;

private observedNodes: Set<string>;
private mutationObserver: MutationObserver | null;

constructor(
window: Window,
private readonly window: Window,
backgroundAction: (action: string, ...args: any[]) => Promise<void>,
useMutationObserver = true,
) {
this.window = window;
this.backgroundAction = backgroundAction;

this.mutationObserver = null;
this.injectedRules = new Set();
this.injectedScripts = new Set();
this.blockedScripts = new Set();

this.observedNodes = new Set();

// Request cosmetics specific to this domain as soon as possible
this.backgroundAction('getCosmeticsForDomain');

if (useMutationObserver) {
// Request cosmetics for nodes already existing in the DOM
this.onMutation([{ target: this.window.document.body }]);

// Register MutationObserver
this.startObserving();
}
}

public unload() {
if (this.mutationObserver) {
try {
this.mutationObserver.disconnect();
} catch (e) {
/* in case the page is closed */
}
}
backgroundAction('getCosmeticsFilters');
}

public injectCircumvention(): void {
Expand All @@ -94,150 +38,27 @@ export default class CosmeticInjection {
styles,
}: IMessageFromBackground) {
if (!active) {
this.unload();
return;
}

// Inject scripts
for (let i = 0; i < scripts.length; i += 1) {
const script = scripts[i];
if (!this.injectedScripts.has(script)) {
injectScript(script, this.window.document);
this.injectedScripts.add(script);
}
injectScript(scripts[i], this.window.document);
}

// Block scripts
for (let i = 0; i < blockedScripts.length; i += 1) {
const script = blockedScripts[i];
if (!this.blockedScripts.has(script)) {
blockScript(script, this.window.document);
this.blockedScripts.add(script);
}
blockScript(blockedScripts[i], this.window.document);
}

// Inject CSS
this.handleRules(styles);
}

private handleRules(rules: string[]) {
const rulesToInject: string[] = [];

// Check which rules should be injected in the page.
for (let i = 0; i < rules.length; i += 1) {
const rule = rules[i];

if (!this.injectedRules.has(rule)) {
// Check if the selector would match
try {
if (!this.window.document.querySelector(rule)) {
continue;
}
} catch (e) {
// invalid selector
continue;
}

this.injectedRules.add(rule);
rulesToInject.push(` :root ${rule}`);
}
}

// Inject selected rules
if (rulesToInject.length > 0) {
injectCSSRule(
`${rulesToInject.join(' ,')} {display:none !important;}`,
this.window.document,
);
}
}

/**
* When one or several mutations occur in the window, extract caracteristics
* (node name, class, tag) from the modified nodes and request matching
* cosmetic filters to inject in the page.
*/
private onMutation(mutations: Array<{ target: Node }>) {
let targets: Set<Node> = new Set(mutations.map((m) => m.target).filter((t) => t));

// TODO - it might be necessary to inject scripts, CSS and block scripts
// from here into iframes with no src. We could first inject/block
// everything already present in `this.injectedRules`,
// `this.injectedScripts` and `this.blockedScripts`. Then we could register
// the iframe to be subjected to the same future injections as the current
// window.
// targets.forEach((target) => {
// if (target.localName === 'iframe') {}
// if (target.childElementCount !== 0) {
// const iframes = target.getElementsByTagName('iframe');
// if (iframes.length !== 0) {}
// }
// });

if (targets.size > 100) {
// In case there are too many mutations we will only check once the whole document
targets = new Set([this.window.document.body]);
}

if (targets.size === 0) {
return;
}

// Collect nodes of targets
const nodeInfo = new Set();
targets.forEach((target) => {
const nodes = (target as HTMLElement).querySelectorAll('*');
for (let i = 0; i < nodes.length; i += 1) {
const node = nodes[i] as HTMLElement;

// Ignore hidden nodes
if (node.hidden) {
continue;
}

if (node.id) {
const selector = `#${node.id}`;
if (!this.observedNodes.has(selector)) {
nodeInfo.add(selector);
this.observedNodes.add(selector);
}
}

if (node.tagName) {
const selector = node.tagName;
if (!this.observedNodes.has(selector)) {
nodeInfo.add(selector);
this.observedNodes.add(selector);
}
}

if (node.className && node.className.split) {
node.className.split(' ').forEach((name) => {
const selector = `.${name}`;
if (!this.observedNodes.has(selector)) {
nodeInfo.add(selector);
this.observedNodes.add(selector);
}
});
}
}
});

// Send node info to background to request corresponding cosmetic filters
if (nodeInfo.size > 0) {
this.backgroundAction('getCosmeticsForNodes', [[...nodeInfo]]);
}
}

private startObserving() {
// Attach mutation observer in case the DOM is mutated.
if (this.window.MutationObserver !== undefined) {
this.mutationObserver = new this.window.MutationObserver((mutations) =>
this.onMutation(mutations),
);
this.mutationObserver.observe(this.window.document, {
childList: true,
subtree: true,
});
}
injectCSSRule(
`${rules.join(',')} { display: none!important; }`,
this.window.document,
);
}
}
2 changes: 2 additions & 0 deletions src/data-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ export default class StaticDataView {
}

this.pos += byteLength;

// @ts-ignore
return String.fromCharCode.apply(null, this.buffer.subarray(this.pos - byteLength, this.pos));
}
}
Loading

0 comments on commit 3f68596

Please sign in to comment.