@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
720 lines (704 loc) • 26.8 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
function __awaiter(thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
/* eslint-disable no-restricted-syntax,no-await-in-loop,no-underscore-dangle */
function waitFor(predicate, maxTimes, interval) {
return __awaiter(this, void 0, void 0, function* () {
let result = false;
try {
result = yield predicate();
}
catch (e) {
console.warn('error in waitFor predicate', e);
}
if (!result && maxTimes > 0) {
return new Promise((resolve) => {
setTimeout(() => __awaiter(this, void 0, void 0, function* () {
resolve(waitFor(predicate, maxTimes - 1, interval));
}), interval);
});
}
return Promise.resolve(result);
});
}
class AutoConsentBase {
constructor(name) {
this.hasSelfTest = true;
this.name = name;
}
detectCmp(tab) {
throw new Error('Not Implemented');
}
detectPopup(tab) {
return __awaiter(this, void 0, void 0, function* () {
return false;
});
}
detectFrame(tab, frame) {
return false;
}
optOut(tab) {
throw new Error('Not Implemented');
}
optIn(tab) {
throw new Error('Not Implemented');
}
openCmp(tab) {
throw new Error('Not Implemented');
}
test(tab) {
return __awaiter(this, void 0, void 0, function* () {
// try IAB by default
yield tab.eval('__cmp(\'getVendorConsents\', undefined, r => window.__rcsResult = r)');
return tab.eval('Object.values(window.__rcsResult.purposeConsents).every(c => !c)');
});
}
}
function evaluateRule(rule, tab) {
return __awaiter(this, void 0, void 0, function* () {
if (rule.frame && !tab.frame) {
yield 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((resolve) => __awaiter(this, void 0, void 0, function* () {
// catch eval error silently
try {
resolve(yield 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 (yield Promise.all(results)).reduce((a, b) => a && b, true);
});
}
class AutoConsent extends AutoConsentBase {
constructor(config) {
super(config.name);
this.config = config;
}
_runRulesParallel(tab, rules) {
return __awaiter(this, void 0, void 0, function* () {
const detections = yield Promise.all(rules.map(rule => evaluateRule(rule, tab)));
return detections.some(r => !!r);
});
}
_runRulesSequentially(tab, rules) {
return __awaiter(this, void 0, void 0, function* () {
for (const rule of rules) {
const result = yield evaluateRule(rule, tab);
if (!result && !rule.optional) {
return false;
}
}
return true;
});
}
detectCmp(tab) {
return __awaiter(this, void 0, void 0, function* () {
if (this.config.detectCmp) {
return this._runRulesParallel(tab, this.config.detectCmp);
}
return false;
});
}
detectPopup(tab) {
return __awaiter(this, void 0, void 0, function* () {
if (this.config.detectPopup) {
return this._runRulesParallel(tab, this.config.detectPopup);
}
return false;
});
}
detectFrame(tab, frame) {
if (this.config.frame) {
return frame.url.startsWith(this.config.frame);
}
return false;
}
optOut(tab) {
return __awaiter(this, void 0, void 0, function* () {
if (this.config.optOut) {
return this._runRulesSequentially(tab, this.config.optOut);
}
return false;
});
}
optIn(tab) {
return __awaiter(this, void 0, void 0, function* () {
if (this.config.optIn) {
return this._runRulesSequentially(tab, this.config.optIn);
}
return false;
});
}
openCmp(tab) {
return __awaiter(this, void 0, void 0, function* () {
if (this.config.openCmp) {
return this._runRulesSequentially(tab, this.config.openCmp);
}
return false;
});
}
test(tab) {
const _super = Object.create(null, {
test: { get: () => super.test }
});
return __awaiter(this, void 0, void 0, function* () {
if (this.config.test) {
return this._runRulesSequentially(tab, this.config.test);
}
return _super.test.call(this, tab);
});
}
}
class Tab {
constructor(page, url, frames) {
// puppeteer doesn't have tab IDs
this.id = 1;
this.page = page;
this.url = url;
this.frames = frames;
}
elementExists(selector, frameId = 0) {
return __awaiter(this, void 0, void 0, function* () {
try {
const elements = yield this.frames[frameId].$$(selector);
return elements.length > 0;
}
catch (e) {
return false;
}
});
}
clickElement(selector, frameId = 0) {
return __awaiter(this, void 0, void 0, function* () {
if (yield this.elementExists(selector, frameId)) {
try {
return yield this.frames[frameId].click(selector);
}
catch (e) {
return false;
}
}
return false;
});
}
clickElements(selector, frameId = 0) {
return __awaiter(this, void 0, void 0, function* () {
const elements = yield this.frames[frameId].$$(selector);
try {
yield Promise.all(elements.map((elem) => elem.click()));
return true;
}
catch (e) {
return false;
}
});
}
elementsAreVisible(selector, check, frameId = 0) {
return __awaiter(this, void 0, void 0, function* () {
if (!(yield this.elementExists(selector, frameId))) {
return false;
}
const visible = yield this.frames[frameId].$$eval(selector, (nodes) => nodes.map((n) => n.offestParent !== null));
if (visible.length === 0) {
return false;
}
else if (check === 'any') {
return visible.some(r => r);
}
else if (check === 'none') {
return visible.every(r => !r);
}
return visible.every(r => r);
});
}
getAttribute(selector, attribute, frameId = 0) {
return __awaiter(this, void 0, void 0, function* () {
const elem = yield this.frames[frameId].$(selector);
if (elem) {
return (yield elem.getProperty(attribute)).jsonValue();
}
});
}
eval(script, frameId = 0) {
return __awaiter(this, void 0, void 0, function* () {
return yield this.frames[frameId].evaluate(script);
});
}
waitForElement(selector, timeout, frameId = 0) {
return __awaiter(this, void 0, void 0, function* () {
const interval = 200;
const times = Math.ceil(timeout / interval);
return waitFor(() => this.elementExists(selector, frameId), times, interval);
});
}
waitForThenClick(selector, timeout, frameId = 0) {
return __awaiter(this, void 0, void 0, function* () {
yield this.waitForElement(selector, timeout, frameId);
yield this.clickElement(selector, frameId);
return true;
});
}
hideElements(selectors, frameId = 0) {
return __awaiter(this, void 0, void 0, function* () {
return Promise.resolve(false);
});
}
goto(url) {
return __awaiter(this, void 0, void 0, function* () {
return this.page.goto(url);
});
}
wait(ms) {
return new Promise((resolve) => {
setTimeout(() => resolve(true), ms);
});
}
matches(options) {
throw new Error("Method not implemented.");
}
executeAction(config, param) {
throw new Error("Method not implemented.");
}
}
class TagCommander extends AutoConsentBase {
constructor() {
super('tagcommander');
}
detectCmp(tab) {
return __awaiter(this, void 0, void 0, function* () {
try {
return yield tab.eval('window.tC && window.tC.privacyCenter !== undefined');
}
catch (e) {
return false;
}
});
}
detectPopup(tab) {
return __awaiter(this, void 0, void 0, function* () {
return (yield tab.elementExists('#dnt-banner')) ||
(yield tab.elementExists('#privacy-iframe')) ||
(yield tab.elementExists('#footer_tc_privacy')) ||
(yield tab.elementExists('#header_tc_privacy'));
});
}
detectFrame(tab, frame) {
return frame.url.startsWith('http://cdn.tagcommander.com/privacy') ||
frame.url.startsWith('https://cdn.tagcommander.com/privacy');
}
openFrame(tab) {
return __awaiter(this, void 0, void 0, function* () {
if ((yield tab.elementExists('#footer_tc_privacy')) ||
(yield tab.elementExists('#footer_tc_privacy_privacy_center')) ||
(yield tab.elementExists('#header_tc_privacy'))) {
yield this.openCmp(tab);
}
});
}
optOut(tab) {
return __awaiter(this, void 0, void 0, function* () {
if (!(yield tab.elementExists('#privacy-iframe'))) {
yield this.openFrame(tab);
yield waitFor(() => !!tab.frame, 10, 200);
}
if (!tab.frame) {
return false;
}
yield tab.wait(500);
yield tab.waitForElement('#privacy-cat-modal', 20000, tab.frame.id);
yield tab.wait(500);
yield tab.clickElements('.btn-yes', tab.frame.id);
yield tab.wait(200);
yield tab.clickElement('.modal-footer > button', tab.frame.id);
return true;
});
}
optIn(tab) {
return __awaiter(this, void 0, void 0, function* () {
if (!(yield tab.elementExists('#privacy-iframe'))) {
yield this.openCmp(tab);
yield waitFor(() => !!tab.frame, 10, 200);
}
if (!tab.frame) {
return false;
}
yield new Promise(resolve => setTimeout(resolve, 500));
yield tab.waitForElement('#privacy-cat-modal', 20000, tab.frame.id);
yield tab.clickElements('.btn-no', tab.frame.id);
yield tab.clickElement('.modal-footer > button', tab.frame.id);
return true;
});
}
openCmp(tab) {
return __awaiter(this, void 0, void 0, function* () {
yield tab.eval('tC.privacyCenter.showPrivacyCenter();');
if (yield tab.waitForElement('#privacy-iframe', 10000)) {
yield new Promise(resolve => setTimeout(resolve, 500));
return true;
}
return false;
});
}
test(tab) {
return __awaiter(this, void 0, void 0, function* () {
return tab.eval('tC.privacy.categories[0] === "-1"');
});
}
}
class TrustArc extends AutoConsentBase {
constructor() {
super('TrustArc');
}
detectFrame(_, frame) {
return frame.url.startsWith('https://consent-pref.trustarc.com/?');
}
detectCmp(tab) {
return __awaiter(this, void 0, void 0, function* () {
if (tab.frame && tab.frame.url.startsWith('https://consent-pref.trustarc.com/?')) {
return true;
}
return tab.elementExists('#truste-show-consent');
});
}
detectPopup(tab) {
return __awaiter(this, void 0, void 0, function* () {
if (tab.frame) {
return tab.waitForElement('#defaultpreferencemanager', 5000, tab.frame.id);
}
return tab.elementExists('#truste-show-consent');
});
}
openFrame(tab) {
return __awaiter(this, void 0, void 0, function* () {
if (yield tab.elementExists('#truste-show-consent')) {
yield tab.clickElement('#truste-show-consent');
}
});
}
navigateToSettings(tab, frameId) {
return __awaiter(this, void 0, void 0, function* () {
// wait for it to load
yield waitFor(() => __awaiter(this, void 0, void 0, function* () {
return (yield tab.elementExists('.shp', frameId)) ||
(yield tab.elementsAreVisible('.advance', 'any', frameId)) ||
tab.elementExists('.switch span:first-child', frameId);
}), 10, 500);
// splash screen -> hit more information
if (yield tab.elementExists('.shp', frameId)) {
yield tab.clickElement('.shp', frameId);
}
// wait for consent options
if (!(yield tab.waitForElement('.switch span:first-child', 2000, frameId))) {
return false;
}
// go to advanced settings if not yet shown
if (yield tab.elementsAreVisible('.advance', 'any', frameId)) {
yield tab.clickElement('.advance', frameId);
}
// takes a while to load the opt-in/opt-out buttons
return yield waitFor(() => tab.elementsAreVisible('.switch span:first-child', 'any', frameId), 5, 1000);
});
}
optOut(tab) {
return __awaiter(this, void 0, void 0, function* () {
yield tab.hideElements(['.truste_overlay', '.truste_box_overlay', '.trustarc-banner', '.truste-banner']);
return true;
});
}
optIn(tab) {
return __awaiter(this, void 0, void 0, function* () {
if (!tab.frame) {
yield this.openFrame(tab);
yield waitFor(() => !!tab.frame, 10, 200);
}
const frameId = tab.frame.id;
yield this.navigateToSettings(tab, frameId);
yield tab.clickElements('.switch span:nth-child(2)', frameId);
yield tab.clickElement('.submit', frameId);
yield waitFor(() => tab.elementExists('#gwt-debug-close_id', frameId), 300, 1000);
yield tab.clickElement('#gwt-debug-close_id', frameId);
return true;
});
}
openCmp(tab) {
return __awaiter(this, void 0, void 0, function* () {
yield tab.eval('truste.eu.clickListener()');
return true;
});
}
test() {
return __awaiter(this, void 0, void 0, function* () {
// TODO: find out how to test TrustArc
return true;
});
}
}
class Cookiebot extends AutoConsentBase {
constructor() {
super('Cybotcookiebot');
}
detectCmp(tab) {
return __awaiter(this, void 0, void 0, function* () {
try {
return yield tab.eval('typeof window.CookieConsent === "object"');
}
catch (e) {
return false;
}
});
}
detectPopup(tab) {
return tab.elementExists('#CybotCookiebotDialog,#dtcookie-container,#cookiebanner');
}
optOut(tab) {
return __awaiter(this, void 0, void 0, function* () {
if (yield tab.elementExists('#dtcookie-container')) {
return tab.clickElement('.h-dtcookie-decline');
}
if (yield tab.elementExists('.cookiebot__button--settings')) {
yield tab.clickElement('.cookiebot__button--settings');
}
if (yield tab.elementsAreVisible('#CybotCookiebotDialogBodyButtonDecline', 'all')) {
return yield tab.clickElement('#CybotCookiebotDialogBodyButtonDecline');
}
yield tab.clickElements('.CybotCookiebotDialogBodyLevelButton:checked:enabled,input[id*="CybotCookiebotDialogBodyLevelButton"]:checked:enabled');
if (yield tab.elementExists('#CybotCookiebotDialogBodyButtonDecline')) {
yield tab.clickElement('#CybotCookiebotDialogBodyButtonDecline');
}
yield tab.clickElements('#CybotCookiebotDialogBodyLevelButtonAccept,#CybotCookiebotDialogBodyButtonAccept');
// await tab.clickElement('#CybotCookiebotDialogBodyButtonAccept');
return true;
});
}
optIn(tab) {
return __awaiter(this, void 0, void 0, function* () {
if (yield tab.elementExists('#dtcookie-container')) {
return tab.clickElement('.h-dtcookie-accept');
}
yield tab.clickElements('.CybotCookiebotDialogBodyLevelButton:not(:checked):enabled');
yield tab.clickElement('#CybotCookiebotDialogBodyLevelButtonAccept');
yield tab.clickElement('#CybotCookiebotDialogBodyButtonAccept');
return true;
});
}
openCmp(tab) {
return __awaiter(this, void 0, void 0, function* () {
yield tab.eval('CookieConsent.renew() || true');
return tab.waitForElement('#CybotCookiebotDialog', 10000);
});
}
test(tab) {
return __awaiter(this, void 0, void 0, function* () {
return tab.eval('CookieConsent.declined === true');
});
}
}
function clickFirstMatching(tab, elements) {
return __awaiter(this, void 0, void 0, function* () {
const exists = yield Promise.all(elements.map(e => tab.elementExists(e)));
const first = exists.findIndex(e => !!e);
if (first >= 0) {
yield tab.clickElement(elements[first]);
return;
}
throw new Error('cannot open CMP');
});
}
const popupContainers = [
'div[class*="popup_popup-"]',
'div[class*="banner_banner-"]',
'div[class*="main_banner-"]',
'.prism-modal-banner',
'div[class*="popupFooter_content-"]'
];
const moreInfoElements = [
'a[class^="banner_learnMore-"]',
'button[class*="intro_rejectAll-"],span[class*="intro_rejectAll-"]',
'.prism-modal-banner .ad-settings',
// link to open options has no class or id, so this might be flaky
'div[class^="banner_message-"] > span:first-child > a',
'div[class^="banner_copy-"] > a',
'div[class^="main_buttons-"] > div > button:first-child',
'div[class^="banner_option-"]:nth-child(3) > a,a[class^="banner_option-"]:nth-child(3)',
];
class AppGdpr extends AutoConsentBase {
constructor() {
super('app_gdpr');
}
detectCmp(tab) {
return tab.elementExists('div[class^="app_gdpr-"]');
}
detectPopup(tab) {
return __awaiter(this, void 0, void 0, function* () {
return !(yield tab.elementExists('div[class*="banner_hidden-"]'))
&& (yield tab.elementsAreVisible(popupContainers.join(','), 'any'))
&& (yield tab.elementExists(moreInfoElements.join(',')))
&& this.detectCmp(tab);
});
}
optOut(tab) {
return __awaiter(this, void 0, void 0, function* () {
yield clickFirstMatching(tab, moreInfoElements);
if (yield tab.elementExists('button[class*="details_btnDeactivate-"],button[class*="introV2_rejectAll-"')) {
yield tab.waitForThenClick('button[class*="details_btnDeactivate-"],button[class*="introV2_rejectAll-"');
return tab.clickElement('button[class*="details_btnSave-"],button[class*="details_save-"]');
}
else if (yield tab.elementExists('div[class*="purposes_purposeItem-"]')) {
let i = 1;
while (yield tab.elementExists(`div[class*="purposes_purposeItem-"]:nth-child(${i})`)) {
yield tab.clickElement(`div[class*="purposes_purposeItem-"]:nth-child(${i})`);
yield tab.clickElements('span[class*="switch_isSelected-"]');
i += 1;
}
}
else if (yield tab.elementExists('span[class*="switch_isSelected-"]')) {
// streamlined UI with categories
yield tab.clickElements('span[class*="switch_isSelected-"]');
}
else {
// we have to turn off vendors for all categories...
let i = 1;
while (yield tab.elementExists(`div[class*="summary_purposeItem-"]:nth-child(${i}) > a`)) {
yield tab.clickElement(`div[class*="summary_purposeItem-"]:nth-child(${i}) > a`);
yield tab.clickElements('span[class*="switch_isSelected-"]');
yield tab.clickElement('button[class*="details_back"]');
i += 1;
}
}
return yield tab.clickElement('button[class*="details_save"]');
});
}
optIn(tab) {
return __awaiter(this, void 0, void 0, function* () {
if (yield tab.elementExists('a[class^="banner_continue-"]')) {
yield tab.clickElement('a[class^="banner_continue-"]');
}
if (yield tab.elementExists('span[class^="banner_consent-"')) {
yield tab.clickElement('span[class^="banner_consent-"');
}
return true;
});
}
}
const domains = [
'techradar.com',
'cyclingnews.com',
'livescience.com',
'gamesradar.com',
'itproportal.com',
];
class Future extends AutoConsentBase {
constructor() {
super('future');
}
detectCmp(tab) {
return __awaiter(this, void 0, void 0, function* () {
return !!((yield tab.elementExists('#cmp-container-id')) && tab.frame);
});
}
detectFrame(tab, frame) {
return domains.some(d => frame.url.startsWith(`https://consent.cmp.${d}/`));
}
detectPopup(tab) {
return tab.elementExists('#cmp-container-id');
}
optIn(tab) {
return __awaiter(this, void 0, void 0, function* () {
if (!(yield waitFor(() => !!tab.frame, 10, 500))) {
throw new Error('no frame');
}
yield waitFor(() => !!tab.frame, 10, 500);
yield tab.waitForElement('#mainMoreInfo', 10000, tab.frame.id);
yield tab.clickElement('#mainMoreInfo', tab.frame.id);
yield tab.clickElement('.cmp-btn-acceptall', tab.frame.id);
yield tab.clickElement('.cmp-btn-acceptall', tab.frame.id);
return true;
});
}
optOut(tab) {
return __awaiter(this, void 0, void 0, function* () {
if (!(yield waitFor(() => !!tab.frame, 10, 500))) {
throw new Error('no frame');
}
yield tab.waitForElement('#mainMoreInfo', 10000, tab.frame.id);
yield tab.clickElement('#mainMoreInfo', tab.frame.id);
yield tab.clickElement('.cmp-btn-rejectall', tab.frame.id);
yield tab.clickElement('#confirmLeave', tab.frame.id);
return true;
});
}
}
const rules = [
new TagCommander(),
new TrustArc(),
new Cookiebot(),
new AppGdpr(),
new Future(),
];
function createAutoCMP(config) {
return new AutoConsent(config);
}
const rules$1 = rules;
exports.Tab = Tab;
exports.createAutoCMP = createAutoCMP;
exports.rules = rules$1;
exports.waitFor = waitFor;