Skip to content

Commit

Permalink
Initial support for extended CSS selectors (a.k.a. procedural filters)
Browse files Browse the repository at this point in the history
  • Loading branch information
remusao committed Jan 21, 2021
1 parent 2058e60 commit 1e735d1
Show file tree
Hide file tree
Showing 41 changed files with 3,582 additions and 455 deletions.
2 changes: 1 addition & 1 deletion .mocharc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module.exports = {
timeout: 10000,
reporter: 'spec',
require: ['ts-node/register'],
retries: 2,
retries: 0,
color: false,
extension: ['ts'],
recursive: true,
Expand Down
143 changes: 81 additions & 62 deletions packages/adblocker-content/adblocker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

import type { AST } from '@cliqz/adblocker-extended-selectors';

const SCRIPT_ID = 'cliqz-adblocker-script';
const IGNORED_TAGS = new Set(['br', 'head', 'link', 'meta', 'script', 'style', 's']);

export type Lifecycle = 'start' | 'dom-update';

export interface IBackgroundCallback {
Expand All @@ -19,36 +24,40 @@ export interface IMessageFromBackground {
active: boolean;
scripts: string[];
styles: string;
extended: string[];
extended: {
ast: AST;
remove: boolean;
attribute?: string | undefined;
}[];
}

export interface DOMElement {
href?: string | SVGAnimatedString | null;
nodeType?: number;
localName?: string;
id?: string;
classList?: DOMTokenList;
querySelectorAll?: ParentNode['querySelectorAll'];
function isElement(node: Node): node is Element {
return node.nodeType === Node.ELEMENT_NODE;
}

export function getDOMElementsFromMutations(mutations: MutationRecord[]): DOMElement[] {
function shouldIgnoreElement(element: Element): boolean {
return IGNORED_TAGS.has(element.nodeName.toLowerCase());
}

function getElementsFromMutations(mutations: MutationRecord[]): Element[] {
// Accumulate all nodes which were updated in `nodes`
const nodes: DOMElement[] = [];
const elements: Element[] = [];

for (const mutation of mutations) {
if (mutation.type === 'attributes') {
nodes.push(mutation.target);
if (isElement(mutation.target)) {
elements.push(mutation.target);
}
} else if (mutation.type === 'childList') {
for (const addedNode of mutation.addedNodes) {
nodes.push(addedNode);

const addedDOMElement: DOMElement = addedNode;
if (addedDOMElement.querySelectorAll !== undefined) {
nodes.push(...addedDOMElement.querySelectorAll('[id],[class],[href]'));
if (isElement(addedNode) && addedNode.id !== SCRIPT_ID) {
elements.push(addedNode);
}
}
}
}
return nodes;

return elements;
}

/**
Expand All @@ -58,44 +67,41 @@ export function getDOMElementsFromMutations(mutations: MutationRecord[]): DOMEle
* more details).
*/
export function extractFeaturesFromDOM(
elements: DOMElement[],
roots: Element[],
): {
classes: string[];
hrefs: string[];
ids: string[];
} {
const ignoredTags = new Set(['br', 'head', 'link', 'meta', 'script', 'style']);
const classes: Set<string> = new Set();
const hrefs: Set<string> = new Set();
const ids: Set<string> = new Set();

for (const element of elements) {
if (element.nodeType !== 1 /* Node.ELEMENT_NODE */) {
continue;
}

if (element.localName !== undefined && ignoredTags.has(element.localName)) {
continue;
}
for (const root of roots) {
for (const element of [root, ...root.querySelectorAll('[id],[class],[href]')]) {
if (shouldIgnoreElement(element)) {
continue;
}

// Update ids
const id = element.id;
if (id) {
ids.add(id);
}
// Update ids
const id = element.id;
if (id) {
ids.add(id);
}

// Update classes
const classList = element.classList;
if (classList) {
for (const cls of classList) {
classes.add(cls);
// Update classes
const classList = element.classList;
if (classList) {
for (const cls of classList) {
classes.add(cls);
}
}
}

// Update href
const href = element.href;
if (typeof href === 'string') {
hrefs.add(href);
// Update href
const href = element.getAttribute('href');
if (typeof href === 'string') {
hrefs.add(href);
}
}
}

Expand All @@ -106,31 +112,46 @@ export function extractFeaturesFromDOM(
};
}

export interface FeaturesUpdate {
type: 'features';
ids: string[];
classes: string[];
hrefs: string[];
}

export interface ElementsUpdate {
type: 'elements';
elements: Element[];
}

export type DOMUpdate = FeaturesUpdate | ElementsUpdate;

export class DOMMonitor {
private knownIds: Set<string> = new Set();
private knownHrefs: Set<string> = new Set();
private knownClasses: Set<string> = new Set();

private observer: MutationObserver | null = null;

constructor(
private readonly cb: (features: { ids: string[]; classes: string[]; hrefs: string[] }) => void,
) {}
constructor(private readonly cb: (update: DOMUpdate) => void) {}

public queryAll(window: Pick<Window, 'document'>): void {
this.handleNewNodes(Array.from(window.document.querySelectorAll('[id],[class],[href]')));
this.cb({ type: 'elements', elements: [window.document.documentElement] });
this.handleUpdatedNodes([window.document.documentElement]);
}

public start(
window: Pick<Window, 'document'> & { MutationObserver?: typeof MutationObserver },
): void {
if (this.observer === null && window.MutationObserver !== undefined) {
this.observer = new window.MutationObserver((mutations: MutationRecord[]) => {
this.handleNewNodes(getDOMElementsFromMutations(mutations));
this.handleUpdatedNodes(getElementsFromMutations(mutations));
});

this.observer.observe(window.document.documentElement, {
attributeFilter: ['class', 'id', 'href'],
// Monitor some attributes
attributes: true,
attributeFilter: ['class', 'id', 'href'],
childList: true,
subtree: true,
});
Expand Down Expand Up @@ -181,6 +202,7 @@ export class DOMMonitor {

if (newIds.length !== 0 || newClasses.length !== 0 || newHrefs.length !== 0) {
this.cb({
type: 'features',
classes: newClasses,
hrefs: newHrefs,
ids: newIds,
Expand All @@ -191,8 +213,16 @@ export class DOMMonitor {
return false;
}

private handleNewNodes(nodes: DOMElement[]): boolean {
return this.handleNewFeatures(extractFeaturesFromDOM(nodes));
private handleUpdatedNodes(elements: Element[]): boolean {
if (elements.length !== 0) {
this.cb({
type: 'elements',
elements: elements.filter((e) => shouldIgnoreElement(e) === false),
});
return this.handleNewFeatures(extractFeaturesFromDOM(elements));
}

return false;
}
}

Expand Down Expand Up @@ -220,21 +250,10 @@ export function autoRemoveScript(script: string): string {
// })();
}

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

export function injectScript(s: string, doc: Document): void {
const script = doc.createElement('script');
script.type = 'text/javascript';
script.id = 'cliqz-adblocker-script';
script.id = SCRIPT_ID;
script.async = false;
script.appendChild(doc.createTextNode(autoRemoveScript(s)));

Expand Down
5 changes: 4 additions & 1 deletion packages/adblocker-content/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,16 @@
"bugs": {
"url": "https://github.com/cliqz-oss/adblocker/issues"
},
"dependencies": {
"@cliqz/adblocker-extended-selectors": "^1.19.0"
},
"devDependencies": {
"@ampproject/rollup-plugin-closure-compiler": "^0.26.0",
"@rollup/plugin-node-resolve": "^11.0.0",
"@types/node": "^14.0.23",
"rimraf": "^3.0.0",
"rollup": "^2.0.0",
"rollup-plugin-sourcemaps": "^0.6.1",
"rollup-plugin-terser": "^7.0.0",
"tslint": "^6.0.0",
"tslint-config-prettier": "^1.18.0",
"tslint-no-unused-expression-chai": "^0.1.4",
Expand Down
9 changes: 5 additions & 4 deletions packages/adblocker-content/rollup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

import compiler from '@ampproject/rollup-plugin-closure-compiler';
import resolve from '@rollup/plugin-node-resolve';
import sourcemaps from 'rollup-plugin-sourcemaps';
import { terser } from 'rollup-plugin-terser';

export default {
input: './dist/es6/adblocker.js',
Expand All @@ -21,9 +21,10 @@ export default {
plugins: [
resolve(),
sourcemaps(),
compiler({
// language: 'ECMASCRIPT6_STRICT',
language_out: 'NO_TRANSPILE',
terser({
output: {
comments: false,
},
}),
],
};
3 changes: 3 additions & 0 deletions packages/adblocker-content/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@
"outDir": "dist/cjs",
"declarationDir": "dist/types"
},
"references": [
{ "path": "../adblocker-extended-selectors/tsconfig.json" },
],
"include": ["./adblocker.ts"]
}
20 changes: 12 additions & 8 deletions packages/adblocker-electron-preload/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ if (window === window.top && window.location.href.startsWith('devtools://') ===

ipcRenderer.on(
'get-cosmetic-filters-response',
(_: Electron.IpcRendererEvent, { active, scripts }: IMessageFromBackground) => {
// TODO - implement extended filtering for Electron
(
_: Electron.IpcRendererEvent,
{ active, scripts /* , extended */ }: IMessageFromBackground,
) => {
if (active === false) {
ACTIVE = false;
unload();
Expand All @@ -56,13 +60,13 @@ if (window === window.top && window.location.href.startsWith('devtools://') ===
window.addEventListener(
'DOMContentLoaded',
() => {
DOM_MONITOR = new DOMMonitor(({ classes, ids, hrefs }) => {
getCosmeticsFilters({
classes,
hrefs,
ids,
lifecycle: 'dom-update',
});
DOM_MONITOR = new DOMMonitor((update) => {
if (update.type === 'features') {
getCosmeticsFilters({
...update,
lifecycle: 'dom-update',
});
}
});

DOM_MONITOR.queryAll(window);
Expand Down
5 changes: 3 additions & 2 deletions packages/adblocker-electron/adblocker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export class ElectronBlocker extends FiltersEngine {
const hostname = parsed.hostname || '';
const domain = parsed.domain || '';

const { active, styles, scripts } = this.getCosmeticsFilters({
const { active, styles, scripts, extended } = this.getCosmeticsFilters({
domain,
hostname,
url,
Expand All @@ -173,6 +173,7 @@ export class ElectronBlocker extends FiltersEngine {
// This needs to be done only once per frame
getBaseRules: msg.lifecycle === 'start',
getInjectionRules: msg.lifecycle === 'start',
getExtendedRules: msg.lifecycle === 'start',
getRulesFromHostname: msg.lifecycle === 'start',

// This will be done every time we get information about DOM mutation
Expand All @@ -189,7 +190,7 @@ export class ElectronBlocker extends FiltersEngine {
// Inject scripts from content script
event.sender.send('get-cosmetic-filters-response', {
active,
extended: [],
extended,
scripts,
styles: '',
} as IMessageFromBackground);
Expand Down
Loading

0 comments on commit 1e735d1

Please sign in to comment.