276 lines
7.2 KiB
JavaScript

if (typeof browser === 'undefined') {
browser = chrome;
}
/**
* @description API URL
* @type {string}
*/
const apiUrl = 'https://api.cookie-dialog-monster.com/rest/v4';
/**
* @description Context menu identifier
* @type {string}
*/
const extensionMenuItemId = 'CDM-MENU';
/**
* @description Context menu identifier
* @type {string}
*/
const reportMenuItemId = 'CDM-REPORT';
/**
* @description Context menu identifier
* @type {string}
*/
const settingsMenuItemId = 'CDM-SETTINGS';
/**
* @description A shortcut for browser.scripting
* @type {browser.scripting}
*/
const script = browser.scripting;
/**
* @description The storage to use
* @type {browser.storage.LocalStorageArea}
*/
const storage = browser.storage.local;
/**
* @description Refresh data
* @param {void?} callback
*/
const refreshData = (callback) => {
try {
fetch(`${apiUrl}/data/`).then((result) => {
result.json().then(({ data }) => {
storage.set({ data }, suppressLastError);
callback?.(data);
});
});
} catch {
refreshData(callback);
}
};
/**
* @async
* @description Report active tab URL
* @param {any} message
* @param {browser.tabs.Tab} tab
* @param {void?} callback
*/
const report = async (message, tab, callback) => {
try {
const reason = message.reason;
const userAgent = message.userAgent;
const version = browser.runtime.getManifest().version;
const body = JSON.stringify({ reason, url: tab.url, userAgent, version });
const headers = { 'Cache-Control': 'no-cache', 'Content-type': 'application/json' };
const url = `${apiUrl}/report/`;
const response = await fetch(url, { body, headers, method: 'POST' });
callback?.((await response.json()).data);
} catch {
console.error("Can't send report");
}
};
/**
* @description Supress `browser.runtime.lastError`
*/
const suppressLastError = () => void browser.runtime.lastError;
/**
* @description Listen to context menus clicked
*/
browser.contextMenus.onClicked.addListener((info, tab) => {
const tabId = tab?.id;
switch (info.menuItemId) {
case reportMenuItemId:
if (tabId !== undefined) {
browser.tabs.sendMessage(tabId, { type: 'SHOW_REPORT_DIALOG' }, suppressLastError);
}
break;
case settingsMenuItemId:
browser.runtime.openOptionsPage();
break;
default:
break;
}
});
/**
* @description Listens to messages
*/
browser.runtime.onMessage.addListener((message, sender, callback) => {
const hostname = message.hostname;
const isPage = sender.frameId === 0;
const tabId = sender.tab?.id;
switch (message.type) {
case 'DISABLE_ICON':
if (isPage && tabId !== undefined) {
browser.action.setIcon({ path: '/assets/icons/disabled.png', tabId }, suppressLastError);
browser.action.setBadgeText({ tabId, text: '' });
}
break;
case 'ENABLE_ICON':
if (isPage && tabId !== undefined) {
browser.action.setIcon({ path: '/assets/icons/enabled.png', tabId }, suppressLastError);
}
break;
case 'ENABLE_POPUP':
if (isPage && tabId !== undefined) {
browser.action.setPopup({ popup: '/popup.html', tabId }, suppressLastError);
}
break;
case 'GET_DATA':
storage.get('data', ({ data }) => {
if (data) {
callback(data);
} else {
refreshData(callback);
}
});
return true;
case 'GET_EXCLUSION_LIST':
storage.get(null, (exclusions) => {
const exclusionList = Object.entries(exclusions || {}).flatMap((exclusion) =>
exclusion[0] !== 'data' && !exclusion[1]?.enabled ? [exclusion[0]] : []
);
callback(exclusionList);
});
return true;
case 'GET_HOSTNAME_STATE':
if (hostname) {
storage.get(hostname, (state) => {
callback(state[hostname] ?? { enabled: true });
});
return true;
}
break;
case 'GET_TAB':
browser.tabs.query({ active: true, currentWindow: true }, (tabs) => {
callback(tabs[0]);
});
return true;
case 'INSERT_DIALOG_CSS':
if (isPage && tabId !== undefined) {
script.insertCSS({ files: ['styles/dialog.css'], target: { tabId } });
}
break;
case 'REFRESH_DATA':
refreshData(callback);
return true;
case 'REPORT':
if (tabId !== undefined) {
report(message, sender.tab, callback);
return true;
}
break;
case 'SET_BADGE':
if (tabId !== undefined) {
browser.action.setBadgeBackgroundColor({ color: '#6b7280' });
browser.action.setBadgeText({ tabId, text: message.value });
}
break;
case 'SET_HOSTNAME_STATE':
if (hostname) {
if (message.state.enabled === false) {
storage.set({ [hostname]: message.state });
} else {
storage.remove(hostname);
}
}
break;
default:
break;
}
});
/**
* @description Listens to extension installed
*/
browser.runtime.onInstalled.addListener(() => {
browser.contextMenus.create(
{
contexts: ['all'],
documentUrlPatterns: browser.runtime.getManifest().content_scripts[0].matches,
id: extensionMenuItemId,
title: 'Cookie Dialog Monster',
},
suppressLastError
);
browser.contextMenus.create(
{
contexts: ['all'],
documentUrlPatterns: browser.runtime.getManifest().content_scripts[0].matches,
id: settingsMenuItemId,
parentId: extensionMenuItemId,
title: browser.i18n.getMessage('contextMenu_settingsOption'),
},
suppressLastError
);
browser.contextMenus.create(
{
contexts: ['all'],
documentUrlPatterns: browser.runtime.getManifest().content_scripts[0].matches,
id: reportMenuItemId,
parentId: extensionMenuItemId,
title: browser.i18n.getMessage('contextMenu_reportOption'),
},
suppressLastError
);
});
/**
* @description Listen to first start
*/
browser.runtime.onStartup.addListener(() => {
refreshData();
});
/**
* @description Listen to the moment before a request is made to apply the rules
*/
browser.webRequest.onBeforeRequest.addListener(
async (details) => {
const { tabId, type, url } = details;
if (tabId > -1 && type === 'main_frame') {
const manifest = browser.runtime.getManifest();
const excludeMatches = manifest.content_scripts[0].exclude_matches;
const excludePatterns = excludeMatches.map(
(match) => `^${match.replaceAll('*.', '*(.)?').replaceAll('*', '.*')}$`
);
if (excludePatterns.some((pattern) => new RegExp(pattern).test(url))) {
return;
}
const hostname = url.split('.').slice(-3).join('.').replace('www.', '');
const store = await storage.get(hostname);
const state = store[hostname] ?? { enabled: true };
if (state.enabled) {
const { data } = await storage.get('data');
if (data?.rules?.length) {
browser.declarativeNetRequest.updateSessionRules({
addRules: data.rules.map((rule) => ({
...rule,
condition: { ...rule.condition, tabIds: [tabId] },
})),
removeRuleIds: data.rules.map((rule) => rule.id),
});
}
}
}
},
{ urls: ['<all_urls>'] }
);