Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix style injection in pages + optimizations #67

Merged
merged 1 commit into from
Dec 4, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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