Skip to content

Commit

Permalink
packages: adblocker-electron{-preload}: rework scriplet injection
Browse files Browse the repository at this point in the history
Signed-off-by: nullptropy <nullptropy@tutanota.com>
  • Loading branch information
nullptropy committed Sep 17, 2024
1 parent d9dc568 commit 22f5b22
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 146 deletions.
67 changes: 18 additions & 49 deletions packages/adblocker-electron-preload/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,20 @@
*/

import { ipcRenderer } from 'electron';
import { DOMMonitor, IBackgroundCallback } from '@cliqz/adblocker-content';

import {
DOMMonitor,
IBackgroundCallback,
IMessageFromBackground,
injectScript,
} from '@cliqz/adblocker-content';

function getCosmeticsFiltersFirst(): string[] | null {
return ipcRenderer.sendSync('get-cosmetic-filters-first', window.location.href);
interface CosmeticFiltersResponse {
active: boolean;
error?: string;
}
function getCosmeticsFiltersUpdate(data: Omit<IBackgroundCallback, 'lifecycle'>) {
ipcRenderer.send('get-cosmetic-filters', window.location.href, data);

function injectCosmeticFilters(data?: Omit<IBackgroundCallback, 'lifecycle'>): Promise<CosmeticFiltersResponse> {
return ipcRenderer.invoke('inject-cosmetic-filters', window.location.href, data);
}

if (window === window.top && window.location.href.startsWith('devtools://') === false) {
(() => {
const enableMutationObserver = ipcRenderer.sendSync('is-mutation-observer-enabled');
if (window === window.top && !window.location.href.startsWith('devtools://')) {
(async () => {
const enableMutationObserver = await ipcRenderer.invoke('is-mutation-observer-enabled');

let ACTIVE: boolean = true;
let DOM_MONITOR: DOMMonitor | null = null;
Expand All @@ -36,50 +32,23 @@ if (window === window.top && window.location.href.startsWith('devtools://') ===
}
};

ipcRenderer.on(
'get-cosmetic-filters-response',
// TODO - implement extended filtering for Electron
(
_: Electron.IpcRendererEvent,
{ active /* , scripts, extended */ }: IMessageFromBackground,
) => {
if (active === false) {
ACTIVE = false;
unload();
return;
}

ACTIVE = true;
},
);

const scripts = getCosmeticsFiltersFirst();
if (scripts) {
for (const script of scripts) {
injectScript(script, document);
}
}
const { active, error } = await injectCosmeticFilters();
if (error) throw new Error(`Error injecting initial cosmetic filters: ${error}`)
if (!(ACTIVE = active)) return;

// On DOMContentLoaded, start monitoring the DOM. This means that we will
// first check which ids and classes exist in the DOM as a one-off operation;
// this will allow the injection of selectors which have a chance to match.
// We also register a MutationObserver which will monitor the addition of new
// classes and ids, and might trigger extra filters on a per-need basis.
window.addEventListener(
'DOMContentLoaded',
() => {
DOM_MONITOR = new DOMMonitor((update) => {
DOM_MONITOR = new DOMMonitor(async (update) => {
if (update.type === 'features') {
getCosmeticsFiltersUpdate({
...update,
});
const { active, error } = await injectCosmeticFilters();
if (error) throw new Error(`error injecting updated cosmetic filters: ${error}`)
if (!(ACTIVE = active)) unload();
}
});

DOM_MONITOR.queryAll(window);

// Start observing mutations to detect new ids and classes which would
// need to be hidden.
if (ACTIVE && enableMutationObserver) {
DOM_MONITOR.start(window);
}
Expand All @@ -92,4 +61,4 @@ if (window === window.top && window.location.href.startsWith('devtools://') ===
}

// Re-export symbols for convenience
export type { IBackgroundCallback, IMessageFromBackground } from '@cliqz/adblocker-content';
export type { IBackgroundCallback } from '@cliqz/adblocker-content';
144 changes: 47 additions & 97 deletions packages/adblocker-electron/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ import * as electron from 'electron';
import { parse } from 'tldts-experimental';

import { ElectronRequestType, FiltersEngine, Request } from '@cliqz/adblocker';
import type {
IBackgroundCallback,
IMessageFromBackground,
} from '@cliqz/adblocker-electron-preload';
import type { IBackgroundCallback } from '@cliqz/adblocker-electron-preload';

import { PRELOAD_PATH } from './preload_path.js';

Expand All @@ -40,7 +37,7 @@ export function fromElectronDetails(
url,
}
: {
_originalRequestDetails: details,
_originalRequestDetails: details,
requestId: `${id}`,
sourceUrl: referrer,
type: (resourceType || 'other') as ElectronRequestType,
Expand All @@ -58,40 +55,36 @@ export class BlockingContext {
callback: (a: Electron.CallbackResponse) => void,
) => void;

private readonly onGetCosmeticFiltersUpdated: (
event: Electron.IpcMainEvent,
private readonly onInjectCosmeticFilters: (
event: Electron.IpcMainInvokeEvent,
url: string,
msg: IBackgroundCallback,
) => void;

private readonly onGetCosmeticFiltersFirst: (event: Electron.IpcMainEvent, url: string) => void;
msg?: IBackgroundCallback,
) => Promise<{ active: boolean; error?: string }>;

private readonly onHeadersReceived: (
details: Electron.OnHeadersReceivedListenerDetails,
callback: (a: Electron.HeadersReceivedResponse) => void,
) => void;

private readonly onIsMutationObserverEnabled: (event: Electron.IpcMainEvent) => void;
private readonly onIsMutationObserverEnabled: (event: Electron.IpcMainInvokeEvent) => Promise<boolean>;

constructor(
private readonly session: Electron.Session,
private readonly blocker: ElectronBlocker,
) {
this.onBeforeRequest = (details, callback) => blocker.onBeforeRequest(details, callback);

this.onGetCosmeticFiltersFirst = (event, url) => blocker.onGetCosmeticFiltersFirst(event, url);
this.onGetCosmeticFiltersUpdated = (event, url, msg) =>
blocker.onGetCosmeticFiltersUpdated(event, url, msg);
this.onInjectCosmeticFilters = (event, url, msg) =>
blocker.onInjectCosmeticFilters(event, url, msg);
this.onHeadersReceived = (details, callback) => blocker.onHeadersReceived(details, callback);
this.onIsMutationObserverEnabled = (event) => blocker.onIsMutationObserverEnabled(event);
}

public enable(): void {
if (this.blocker.config.loadCosmeticFilters === true) {
this.session.setPreloads(this.session.getPreloads().concat([PRELOAD_PATH]));
ipcMain.on('get-cosmetic-filters-first', this.onGetCosmeticFiltersFirst);
ipcMain.on('get-cosmetic-filters', this.onGetCosmeticFiltersUpdated);
ipcMain.on('is-mutation-observer-enabled', this.onIsMutationObserverEnabled);
ipcMain.handle('inject-cosmetic-filters', this.onInjectCosmeticFilters);
ipcMain.handle('is-mutation-observer-enabled', this.onIsMutationObserverEnabled);
}

if (this.blocker.config.loadNetworkFilters === true) {
Expand All @@ -116,7 +109,8 @@ export class BlockingContext {

if (this.blocker.config.loadCosmeticFilters === true) {
this.session.setPreloads(this.session.getPreloads().filter((p) => p !== PRELOAD_PATH));
ipcMain.removeListener('get-cosmetic-filters', this.onGetCosmeticFiltersUpdated);
ipcMain.removeHandler('inject-cosmetic-filters');
ipcMain.removeHandler('is-mutation-observer-enabled');
}
}
}
Expand Down Expand Up @@ -164,101 +158,65 @@ export class ElectronBlocker extends FiltersEngine {
// ElectronBlocker-specific additions to FiltersEngine
// ----------------------------------------------------------------------- //

public onIsMutationObserverEnabled = (event: Electron.IpcMainEvent): void => {
event.returnValue = this.config.enableMutationObserver;
public onIsMutationObserverEnabled = async (_: Electron.IpcMainInvokeEvent): Promise<boolean> => {
return this.config.enableMutationObserver;
};

public onGetCosmeticFiltersFirst = (event: Electron.IpcMainEvent, url: string) => {
// Extract hostname from sender's URL
public onInjectCosmeticFilters = async (
event: Electron.IpcMainInvokeEvent,
url: string,
msg?: IBackgroundCallback,
): Promise<{ active: boolean; error?: string }> => {
const parsed = parse(url);
const hostname = parsed.hostname || '';
const domain = parsed.domain || '';

// `msg` is undefined for the initial call and present for subsequent updates
const { active, styles, scripts, extended } = this.getCosmeticsFilters({
domain,
hostname,
url,

// This needs to be done only once per frame
getBaseRules: true,
getInjectionRules: true,
getExtendedRules: true,
getRulesFromHostname: true,
getRulesFromDOM: false, // Only done on updates (see `onGetCosmeticFiltersUpdated`)
// DOM information, only available for updates
classes: msg?.classes,
hrefs: msg?.hrefs,
ids: msg?.ids,

// Rules to fetch: true for initial call, false for updates
getBaseRules: !msg,
getInjectionRules: !msg,
getExtendedRules: !msg,
getRulesFromHostname: !msg,

// Only true for update calls when we have DOM information
getRulesFromDOM: !!msg,

callerContext: {
frameId: event.frameId,
processId: event.processId,
lifecycle: msg?.lifecycle,
},
});

if (active === false) {
event.returnValue = null;
return;
return { active: false };
}

// Inject custom stylesheets
this.injectStyles(event.sender, styles);

event.sender.send('get-cosmetic-filters-response', {
active,
extended,
styles: '',
} as IMessageFromBackground);

// to execute Inject scripts synchronously, simply return scripts to renderer.
event.returnValue = scripts;
};

public onGetCosmeticFiltersUpdated = (
event: Electron.IpcMainEvent,
url: string,
msg: IBackgroundCallback,
): void => {
// Extract hostname from sender's URL
const parsed = parse(url);
const hostname = parsed.hostname || '';
const domain = parsed.domain || '';

const { active, styles, extended } = this.getCosmeticsFilters({
domain,
hostname,
url,

classes: msg.classes,
hrefs: msg.hrefs,
ids: msg.ids,

// Only done on first load in the frame, disable for updates
getBaseRules: false,
getInjectionRules: false,
getExtendedRules: false,
getRulesFromHostname: false,

// This will be done every time we get information about DOM mutation
getRulesFromDOM: true,
try {
if (styles.length > 0) {
await event.sender.insertCSS(styles, { cssOrigin: 'user' });
}

callerContext: {
frameId: event.frameId,
processId: event.processId,
for (const script of scripts) {
await event.sender.executeJavaScript(script, true);
}

lifecycle: msg.lifecycle,
},
});
if (extended.length > 0) { }

if (active === false) {
return;
return { active: true };
} catch (error) {
return { active: true, error: String(error) };
}

// Inject custom stylesheets
this.injectStyles(event.sender, styles);

// Inject scripts from content script
event.sender.send('get-cosmetic-filters-response', {
active,
extended,
styles: '',
} as IMessageFromBackground);
};

public onHeadersReceived = (
Expand Down Expand Up @@ -316,14 +274,6 @@ export class ElectronBlocker extends FiltersEngine {
callback({});
}
};

private injectStyles(sender: Electron.WebContents, styles: string): void {
if (styles.length > 0) {
sender.insertCSS(styles, {
cssOrigin: 'user',
});
}
}
}

// re-export @cliqz/adblocker symbols for convenience
Expand Down

0 comments on commit 22f5b22

Please sign in to comment.