@cliqz/autoconsent
Version:
This is a library of rules for navigating through common consent popups on the web. These rules can be run in a Firefox webextension, or in a puppeteer orchestrated headless browser. Using these rules, opt-in and opt-out options can be selected automatica
187 lines (160 loc) • 4.93 kB
text/typescript
/* eslint-disable no-restricted-syntax,no-await-in-loop,no-underscore-dangle */
import { AutoCMP, TabActor } from "../types";
import { AutoConsentCMPRule, AutoConsentRuleStep } from "../rules";
export async function waitFor(predicate: () => Promise<boolean> | boolean, maxTimes: number, interval: number): Promise<boolean> {
let result = false;
try {
result = await predicate();
} catch (e) {
console.warn('error in waitFor predicate', e);
}
if (!result && maxTimes > 0) {
return new Promise((resolve) => {
setTimeout(async () => {
resolve(waitFor(predicate, maxTimes - 1, interval));
}, interval);
});
}
return Promise.resolve(result);
}
export default class AutoConsentBase implements AutoCMP {
name: string
hasSelfTest = true
constructor(name: string) {
this.name = name;
}
detectCmp(tab: TabActor): Promise<boolean> {
throw new Error('Not Implemented');
}
async detectPopup(tab: TabActor) {
return false;
}
detectFrame(tab: TabActor, frame: { url: string }) {
return false;
}
optOut(tab: TabActor): Promise<boolean> {
throw new Error('Not Implemented');
}
optIn(tab: TabActor): Promise<boolean> {
throw new Error('Not Implemented');
}
openCmp(tab: TabActor): Promise<boolean> {
throw new Error('Not Implemented');
}
async test(tab: TabActor): Promise<boolean> {
// try IAB by default
await tab.eval('__cmp(\'getVendorConsents\', undefined, r => window.__rcsResult = r)');
return tab.eval('Object.values(window.__rcsResult.purposeConsents).every(c => !c)');
}
}
async function evaluateRule(rule: AutoConsentRuleStep, tab: TabActor) {
if (rule.frame && !tab.frame) {
await waitFor(() => Promise.resolve(!!tab.frame), 10, 500);
}
const frameId = rule.frame ? tab.frame!!.id : undefined;
const results = [];
if (rule.exists) {
results.push(tab.elementExists(rule.exists, frameId));
}
if (rule.visible) {
results.push(tab.elementsAreVisible(rule.visible, rule.check, frameId));
}
if (rule.eval) {
results.push(new Promise(async (resolve) => {
// catch eval error silently
try {
resolve(await tab.eval(rule.eval!, frameId));
} catch (e) {
resolve(false);
}
}));
}
if (rule.waitFor) {
results.push(tab.waitForElement(rule.waitFor, rule.timeout || 10000, frameId));
}
if (rule.click) {
if (rule.all === true) {
results.push(tab.clickElements(rule.click, frameId));
} else {
results.push(tab.clickElement(rule.click, frameId));
}
}
if (rule.waitForThenClick) {
results.push(tab.waitForElement(rule.waitForThenClick, rule.timeout || 10000, frameId)
.then(() => tab.clickElement(rule.waitForThenClick!, frameId)));
}
if (rule.wait) {
results.push(tab.wait(rule.wait));
}
if (rule.url) {
results.push(Promise.resolve(tab.url.startsWith(rule.url)));
}
if (rule.goto) {
results.push(tab.goto(rule.goto));
}
if (rule.hide) {
results.push(tab.hideElements(rule.hide, frameId));
}
// boolean and of results
return (await Promise.all(results)).reduce((a, b) => a && b, true);
}
export class AutoConsent extends AutoConsentBase {
constructor(public config: AutoConsentCMPRule) {
super(config.name);
}
async _runRulesParallel(tab: TabActor, rules: AutoConsentRuleStep[]): Promise<boolean> {
const detections = await Promise.all(rules.map(rule => evaluateRule(rule, tab)));
return detections.some(r => !!r);
}
async _runRulesSequentially(tab: TabActor, rules: AutoConsentRuleStep[]): Promise<boolean> {
for (const rule of rules) {
const result = await evaluateRule(rule, tab);
if (!result && !rule.optional) {
return false;
}
}
return true;
}
async detectCmp(tab: TabActor) {
if (this.config.detectCmp) {
return this._runRulesParallel(tab, this.config.detectCmp);
}
return false;
}
async detectPopup(tab: TabActor) {
if (this.config.detectPopup) {
return this._runRulesParallel(tab, this.config.detectPopup);
}
return false;
}
detectFrame(tab: TabActor, frame: { url: string }) {
if (this.config.frame) {
return frame.url.startsWith(this.config.frame);
}
return false;
}
async optOut(tab: TabActor) {
if (this.config.optOut) {
return this._runRulesSequentially(tab, this.config.optOut);
}
return false;
}
async optIn(tab: TabActor) {
if (this.config.optIn) {
return this._runRulesSequentially(tab, this.config.optIn);
}
return false;
}
async openCmp(tab: TabActor) {
if (this.config.openCmp) {
return this._runRulesSequentially(tab, this.config.openCmp);
}
return false;
}
async test(tab: TabActor) {
if (this.config.test) {
return this._runRulesSequentially(tab, this.config.test);
}
return super.test(tab);
}
}