MV3 compatibility
This commit is contained in:
parent
501f2c96ce
commit
404d6030e7
|
@ -20,6 +20,7 @@
|
|||
|
||||
// depends on /nscl/common/sha256.js
|
||||
// depends on /nscl/common/uuid.js
|
||||
// depends on /nscl/service/Scripting.js
|
||||
|
||||
"use strict";
|
||||
|
||||
|
@ -56,7 +57,7 @@ var LifeCycle = (() => {
|
|||
async createAndStore() {
|
||||
let allSeen = {};
|
||||
let tab;
|
||||
await Promise.all((await browser.tabs.query({})).map(
|
||||
await Promise.allSettled((await browser.tabs.query({})).map(
|
||||
async t => {
|
||||
let seen = await ns.collectSeen(t.id);
|
||||
if (seen) {
|
||||
|
@ -215,7 +216,7 @@ var LifeCycle = (() => {
|
|||
destroyIfNeeded();
|
||||
if (ns.initializing) await ns.initializing;
|
||||
ns.policy = new Policy(policy);
|
||||
await Promise.all(
|
||||
await Promise.allSettled(
|
||||
Object.entries(allSeen).map(
|
||||
async ([tabId, seen]) => {
|
||||
try {
|
||||
|
@ -240,20 +241,20 @@ var LifeCycle = (() => {
|
|||
if (!UA.isMozilla) {
|
||||
// Chromium does not inject content scripts at startup automatically for already loaded pages,
|
||||
// let's hack it manually.
|
||||
let contentScripts = browser.runtime.getManifest().content_scripts.find(s =>
|
||||
const contentScripts = browser.runtime.getManifest().content_scripts.find(s =>
|
||||
s.js && s.matches.includes("<all_urls>") && s.all_frames && s.match_about_blank).js;
|
||||
|
||||
await Promise.all((await browser.tabs.query({})).map(async tab => {
|
||||
for (let file of contentScripts) {
|
||||
await Promise.allSettled((await browser.tabs.query({})).map(async tab => {
|
||||
try {
|
||||
await browser.tabs.executeScript(tab.id, {file, allFrames: true, matchAboutBlank: true});
|
||||
await Scripting.executeScript({
|
||||
target: {tabId: tab.id, allFrames: true},
|
||||
files: contentScripts,
|
||||
});
|
||||
} catch (e) {
|
||||
await include("/nscl/common/restricted.js");
|
||||
if (!isRestrictedURL(tab.url)) {
|
||||
error(e, "Can't run content script on tab", tab);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -20,17 +20,11 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
function ReportingCSP(marker, reportURI = "") {
|
||||
const DOM_SUPPORTED = "SecurityPolicyViolationEvent" in window;
|
||||
|
||||
if (DOM_SUPPORTED) reportURI = "";
|
||||
function ReportingCSP(marker) {
|
||||
|
||||
return Object.assign(
|
||||
new CapsCSP(new NetCSP(
|
||||
reportURI ? `report-uri ${reportURI}` : marker
|
||||
)),
|
||||
new CapsCSP(new NetCSP(marker)),
|
||||
{
|
||||
reportURI,
|
||||
patchHeaders(responseHeaders, capabilities) {
|
||||
let header = null;
|
||||
let blocker;
|
||||
|
|
|
@ -18,13 +18,12 @@
|
|||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
var RequestGuard = (() => {
|
||||
{
|
||||
'use strict';
|
||||
const VERSION_LABEL = `NoScript ${browser.runtime.getManifest().version}`;
|
||||
browser.browserAction.setTitle({title: VERSION_LABEL});
|
||||
const CSP_REPORT_URI = "https://noscript-csp.invalid/__NoScript_Probe__/";
|
||||
const CSP_MARKER = "noscript-marker";
|
||||
let csp = new ReportingCSP(CSP_MARKER, CSP_REPORT_URI);
|
||||
browser.action.setTitle({title: VERSION_LABEL});
|
||||
const CSP_MARKER = "report-to noscript-reports";
|
||||
const csp = new ReportingCSP(CSP_MARKER);
|
||||
|
||||
const policyTypesMap = {
|
||||
main_frame: "",
|
||||
|
@ -49,6 +48,32 @@ var RequestGuard = (() => {
|
|||
}
|
||||
|
||||
const TabStatus = {
|
||||
_session: new SessionCache(
|
||||
"RequestGuard.TabStatus",
|
||||
{
|
||||
afterLoad(data) {
|
||||
if (data) {
|
||||
TabStatus.map = new Map(data.map);
|
||||
TabStatus._originsCache = new Map(data._originsCache);
|
||||
}
|
||||
},
|
||||
beforeSave() { // beforeSave
|
||||
return {
|
||||
map: [...TabStatus.map],
|
||||
_originsCache: [...TabStatus._originsCache],
|
||||
};
|
||||
},
|
||||
}
|
||||
),
|
||||
init() {
|
||||
for (const event of ["Activated", "Updated", "Removed"]) {
|
||||
browser.tabs[`on${event}`].addListener(TabStatus[`on${event}Tab`]);
|
||||
}
|
||||
(async () => {
|
||||
await TabStatus._session.load();
|
||||
TabStatus.updateTab();
|
||||
});
|
||||
},
|
||||
map: new Map(),
|
||||
_originsCache: new Map(),
|
||||
types: ["script", "object", "media", "frame", "font"],
|
||||
|
@ -89,6 +114,7 @@ var RequestGuard = (() => {
|
|||
initTab(tabId, records = this.newRecords()) {
|
||||
if (tabId < 0) return;
|
||||
this.map.set(tabId, records);
|
||||
this._session.save();
|
||||
return records;
|
||||
},
|
||||
_record(request, what, optValue) {
|
||||
|
@ -121,6 +147,7 @@ var RequestGuard = (() => {
|
|||
collection[type] = [requestKey];
|
||||
}
|
||||
}
|
||||
this._session.save();
|
||||
return records;
|
||||
},
|
||||
record(request, what, optValue) {
|
||||
|
@ -132,10 +159,11 @@ var RequestGuard = (() => {
|
|||
}
|
||||
},
|
||||
_pendingTabs: new Set(),
|
||||
updateTab(tabId) {
|
||||
if (tabId < 0) return;
|
||||
async updateTab(tabId) {
|
||||
tabId ??= (await browser.tabs.getCurrent())?.tabId;
|
||||
if (!(tabId >= 0)) return;
|
||||
if (this._pendingTabs.size === 0) {
|
||||
window.setTimeout(() => { // clamp UI updates
|
||||
setTimeout(() => { // clamp UI updates
|
||||
for (let tabId of this._pendingTabs) {
|
||||
this._updateTabNow(tabId);
|
||||
}
|
||||
|
@ -166,22 +194,22 @@ var RequestGuard = (() => {
|
|||
: (numAllowed ? "sub" : "no")) // not topAllowed
|
||||
: "global"; // not enforced
|
||||
let showBadge = ns.local.showCountBadge && numBlocked > 0;
|
||||
let browserAction = browser.browserAction;
|
||||
if (!browserAction.setIcon) { // Fennec
|
||||
browserAction.setTitle({tabId, title: `NoScript (${numBlocked})`});
|
||||
let {action} = browser;
|
||||
if (!action.setIcon) { // Fennec
|
||||
action.setTitle({tabId, title: `NoScript (${numBlocked})`});
|
||||
return;
|
||||
}
|
||||
(async () => {
|
||||
let iconPath = (await Themes.isVintage()) ? '/img/vintage' : '/img';
|
||||
browserAction.setIcon({tabId, path: {64: `${iconPath}/ui-${icon}64.png`}});
|
||||
action.setIcon({tabId, path: {64: `${iconPath}/ui-${icon}64.png`}});
|
||||
})();
|
||||
|
||||
browserAction.setBadgeText({
|
||||
action.setBadgeText({
|
||||
tabId,
|
||||
text: TabGuard.isAnonymizedTab(tabId) ? "TG" : showBadge ? numBlocked.toString() : ""
|
||||
});
|
||||
browserAction.setBadgeBackgroundColor({tabId, color: [128, 0, 0, 160]});
|
||||
browserAction.setTitle({tabId,
|
||||
action.setBadgeBackgroundColor({tabId, color: [128, 0, 0, 160]});
|
||||
action.setTitle({tabId,
|
||||
title: UA.mobile ? "NoScript" : `${VERSION_LABEL} \n${enforced ?
|
||||
_("BlockedItems", [numBlocked, numAllowed + numBlocked]) + ` \n${report}`
|
||||
: _("NotEnforced")}`
|
||||
|
@ -233,16 +261,15 @@ var RequestGuard = (() => {
|
|||
TabStatus._originsCache.clear();
|
||||
TabStatus._pendingTabs.delete(tabId);
|
||||
},
|
||||
}
|
||||
for (let event of ["Activated", "Updated", "Removed"]) {
|
||||
browser.tabs[`on${event}`].addListener(TabStatus[`on${event}Tab`]);
|
||||
}
|
||||
};
|
||||
TabStatus.init();
|
||||
|
||||
const messageHandler = {
|
||||
|
||||
let messageHandler = {
|
||||
async pageshow(message, sender) {
|
||||
if (sender.frameId === 0) {
|
||||
TabStatus.recordAll(sender.tab.id, message.seen);
|
||||
} else {
|
||||
} else if (sender.tab) {
|
||||
// merge subframes records back into main frame's seen report
|
||||
const tabId = sender.tab.id;
|
||||
for (const {request, allowed, policyType} of message.seen) {
|
||||
|
@ -253,20 +280,40 @@ var RequestGuard = (() => {
|
|||
}
|
||||
return true;
|
||||
},
|
||||
violation({url, type}, sender) {
|
||||
let tabId = sender.tab.id;
|
||||
let {frameId} = sender;
|
||||
let r = {
|
||||
url, type, tabId, frameId, documentUrl: sender.url
|
||||
|
||||
// returns true if it's a true violation (request should be blocked)
|
||||
violation({url, type, isReport}, sender) {
|
||||
const {tab, frameId} = sender;
|
||||
const documentUrl = sender.url;
|
||||
|
||||
let request = {
|
||||
url,
|
||||
type,
|
||||
tabId: tab.id,
|
||||
tabUrl: tab.url,
|
||||
frameId,
|
||||
documentUrl,
|
||||
originUrl: documentUrl,
|
||||
};
|
||||
Content.reportTo(r, false, policyTypesMap[type]);
|
||||
debug("Violation", type, url, tabId, frameId); // DEV_ONLY
|
||||
if (type === "script" && url === sender.url) {
|
||||
TabStatus.record(r, "noscriptFrame", true);
|
||||
} else {
|
||||
TabStatus.record(r, "blocked");
|
||||
|
||||
debug("CSP", isReport ? "report" : "violation", request, sender); // DEV_ONLY
|
||||
|
||||
if (isReport && !checkRequest(request)?.cancel) {
|
||||
// not a real violation
|
||||
return false;
|
||||
}
|
||||
|
||||
Content.reportTo(request, false, policyTypesMap[type]);
|
||||
|
||||
if (type === "script" && url === sender.url) {
|
||||
TabStatus.record(request, "noscriptFrame", true);
|
||||
} else {
|
||||
TabStatus.record(request, "blocked");
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
async blockedObjects(message, sender) {
|
||||
let {url, documentUrl, policyType} = message;
|
||||
let TAG = `<${policyType.toUpperCase()}>`;
|
||||
|
@ -313,7 +360,8 @@ var RequestGuard = (() => {
|
|||
}
|
||||
return {enable: key};
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const Content = {
|
||||
async reportTo(request, allowed, policyType) {
|
||||
let {requestId, tabId, frameId, type, url, documentUrl, originUrl} = request;
|
||||
|
@ -378,9 +426,9 @@ var RequestGuard = (() => {
|
|||
|
||||
function fakeOriginFromTab({tabId, type} = request) {
|
||||
if (type !== "main_frame") {
|
||||
let tab = tabId !== -1 && TabCache.get(tabId);
|
||||
if (tab) {
|
||||
return request.initiator = request.originUrl = request.documentUrl = tab.url;
|
||||
let tabUrl = request.tabUrl || tabId !== -1 && TabCache.get(tabId)?.url;
|
||||
if (tabUrl) {
|
||||
return request.initiator = request.originUrl = request.documentUrl = tabUrl;
|
||||
}
|
||||
}
|
||||
return request.initiator || request.originUrl;
|
||||
|
@ -411,14 +459,11 @@ var RequestGuard = (() => {
|
|||
};
|
||||
|
||||
function intersectCapabilities(perms, request) {
|
||||
let {frameId, frameAncestors, tabId} = request;
|
||||
if (frameId !== 0 && ns.sync.cascadeRestrictions) {
|
||||
let topUrl = frameAncestors && frameAncestors.length
|
||||
&& frameAncestors[frameAncestors.length - 1].url;
|
||||
if (!topUrl) {
|
||||
let tab = TabCache.get(tabId);
|
||||
if (tab) topUrl = tab.url;
|
||||
}
|
||||
if (request.frameId !== 0 && ns.sync.cascadeRestrictions) {
|
||||
const {tabUrl, frameAncestors} = request;
|
||||
const topUrl = tabUrl ||
|
||||
frameAncestors && frameAncestors[frameAncestors?.length - 1]?.url ||
|
||||
TabCache.get(request.tabId)?.url;
|
||||
if (topUrl) {
|
||||
return ns.policy.cascadeRestrictions(perms, topUrl).capabilities;
|
||||
}
|
||||
|
@ -426,7 +471,9 @@ var RequestGuard = (() => {
|
|||
return perms.capabilities;
|
||||
}
|
||||
|
||||
const ABORT = {cancel: true}, ALLOW = {};
|
||||
const ABORT = {cancel: true},
|
||||
ALLOW = {};
|
||||
|
||||
const recent = {
|
||||
MAX_AGE: 500,
|
||||
_pendingGC: 0,
|
||||
|
@ -448,6 +495,7 @@ var RequestGuard = (() => {
|
|||
return null;
|
||||
},
|
||||
add(request) {
|
||||
request.timeStamp ??= Date.now();
|
||||
let last = this._byUrl.get(request.url);
|
||||
if (!last) {
|
||||
last = [request];
|
||||
|
@ -511,17 +559,18 @@ var RequestGuard = (() => {
|
|||
}
|
||||
}
|
||||
|
||||
const listeners = {
|
||||
onBeforeRequest(request) {
|
||||
try {
|
||||
if (browser.runtime.onSyncMessage && browser.runtime.onSyncMessage.isMessageRequest(request)) return ALLOW;
|
||||
normalizeRequest(request);
|
||||
initPendingRequest(request);
|
||||
// returns null if request.type is unknown, otherwise either ALLOW, ABORT or a redirect response
|
||||
function checkRequest(request) {
|
||||
if (!request.type in policyTypesMap) {
|
||||
return null;
|
||||
}
|
||||
|
||||
normalizeRequest(request);
|
||||
|
||||
let {policy} = ns
|
||||
let {tabId, type, url, originUrl} = request;
|
||||
|
||||
if (type in policyTypesMap) {
|
||||
const {policy} = ns
|
||||
|
||||
let previous = recent.find(request);
|
||||
if (previous) {
|
||||
debug("Rapid fire request", previous); // DEV_ONLY
|
||||
|
@ -585,12 +634,26 @@ var RequestGuard = (() => {
|
|||
if (type !== "main_frame") {
|
||||
Content.reportTo(request, allowed, policyType);
|
||||
}
|
||||
|
||||
if (!allowed) {
|
||||
debug(`Blocking ${policyType}`, request);
|
||||
debug(`${policyType} must be blocked`, request);
|
||||
TabStatus.record(request, "blocked");
|
||||
return previous.return = ABORT;
|
||||
}
|
||||
|
||||
return ALLOW;
|
||||
}
|
||||
|
||||
const listeners = {
|
||||
onBeforeRequest(request) {
|
||||
try {
|
||||
if (browser.runtime?.onSyncMessage.isMessageRequest(request)) return ALLOW;
|
||||
|
||||
initPendingRequest(request);
|
||||
|
||||
let result = checkRequest(request);
|
||||
if (result) return result;
|
||||
|
||||
} catch (e) {
|
||||
error(e);
|
||||
}
|
||||
|
@ -695,7 +758,7 @@ var RequestGuard = (() => {
|
|||
|
||||
promises = promises.filter(p => p instanceof Promise);
|
||||
if (promises.length > 0) {
|
||||
return Promise.all(promises).then(() => result);
|
||||
return Promise.allSettled(promises).then(() => result);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
@ -708,6 +771,9 @@ var RequestGuard = (() => {
|
|||
TabStatus.initTab(tabId);
|
||||
TabGuard.onCleanup(request);
|
||||
}
|
||||
if (!RequestGuard.canBlock) {
|
||||
return;
|
||||
}
|
||||
let scriptBlocked = request.responseHeaders.some(
|
||||
h => csp.isMine(h) && csp.blocks(h.value, "script")
|
||||
);
|
||||
|
@ -715,7 +781,6 @@ var RequestGuard = (() => {
|
|||
TabStatus.record(request, "noscriptFrame", scriptBlocked);
|
||||
let pending = pendingRequests.get(requestId);
|
||||
if (pending) {
|
||||
|
||||
pending.scriptBlocked = scriptBlocked;
|
||||
if (!(pending.headersProcessed &&
|
||||
(scriptBlocked || ns.requestCan(request, "script"))
|
||||
|
@ -746,81 +811,64 @@ var RequestGuard = (() => {
|
|||
TabGuard.onCleanup(request);
|
||||
}
|
||||
};
|
||||
function fakeRequestFromCSP(report, request) {
|
||||
let type = report["violated-directive"].split("-", 1)[0]; // e.g. script-src 'none' => script
|
||||
if (type === "frame") type = "sub_frame";
|
||||
let url = report['blocked-uri'];
|
||||
if (!url || url === 'self') url = request.documentUrl;
|
||||
return Object.assign({}, request, {
|
||||
url,
|
||||
type,
|
||||
});
|
||||
}
|
||||
|
||||
let utf8Decoder = new TextDecoder("UTF-8");
|
||||
function onViolationReport(request) {
|
||||
try {
|
||||
let text = utf8Decoder.decode(request.requestBody.raw[0].bytes);
|
||||
if (text.includes(`"inline"`)) return ABORT;
|
||||
let report = JSON.parse(text)["csp-report"];
|
||||
let originalPolicy = report["original-policy"]
|
||||
debug("CSP report", report); // DEV_ONLY
|
||||
let blockedURI = report['blocked-uri'];
|
||||
if (blockedURI && blockedURI !== 'self') {
|
||||
let r = fakeRequestFromCSP(report, request);
|
||||
if (!/:/.test(r.url)) r.url = request.documentUrl;
|
||||
Content.reportTo(r, false, policyTypesMap[r.type]);
|
||||
TabStatus.record(r, "blocked");
|
||||
} else if (report["violated-directive"].startsWith("script-src") && (originalPolicy.includes("script-src 'none'"))) {
|
||||
let r = fakeRequestFromCSP(report, request);
|
||||
Content.reportTo(r, false, "script"); // NEW
|
||||
TabStatus.record(r, "noscriptFrame", true);
|
||||
}
|
||||
} catch(e) {
|
||||
error(e);
|
||||
}
|
||||
return ABORT;
|
||||
}
|
||||
|
||||
function injectPolicyScript(details) {
|
||||
let {url, tabId, frameId} = details;
|
||||
let policy = ns.computeChildPolicy({url}, {tab: {id: tabId}, frameId});
|
||||
policy.navigationURL = url;
|
||||
const {url, tabId, frameId} = details;
|
||||
const domPolicy = ns.computeChildPolicy({url}, {tab: {id: tabId}, frameId});
|
||||
domPolicy.navigationURL = url;
|
||||
const callback = "ns_setupCallback";
|
||||
if (DocStartInjection.mv3Callbacks) {
|
||||
return {
|
||||
data: {domPolicy},
|
||||
callback,
|
||||
assign: "ns",
|
||||
};
|
||||
}
|
||||
let debugStatement = ns.local.debug ? `
|
||||
let mark = Date.now() + ":" + Math.random();
|
||||
console.debug("domPolicy", domPolicy, document.readyState, location.href, mark, window.ns);` : '';
|
||||
return `
|
||||
let domPolicy = ${JSON.stringify(policy)};
|
||||
let {ns} = window;
|
||||
if (ns) {
|
||||
ns.domPolicy = domPolicy;
|
||||
if (ns.setup) {
|
||||
if (ns.syncSetup) ns.syncSetup(domPolicy);
|
||||
else ns.setup(domPolicy);
|
||||
} ;
|
||||
const domPolicy = ${JSON.stringify(domPolicy)};
|
||||
if (globalThis.${callback}) {
|
||||
globalThis.${callback}(domPolicy);
|
||||
} else {
|
||||
window.ns = {domPolicy}
|
||||
globalThis.ns ||= {domPolicy}
|
||||
}
|
||||
${debugStatement}`;
|
||||
}
|
||||
|
||||
const RequestGuard = {
|
||||
async start() {
|
||||
// external interface
|
||||
globalThis.RequestGuard = {
|
||||
canBlock: UA.isMozilla,
|
||||
DNRPolicy: null,
|
||||
policyTypesMap,
|
||||
};
|
||||
|
||||
// initialization
|
||||
{
|
||||
Messages.addHandler(messageHandler);
|
||||
let wr = browser.webRequest;
|
||||
let listen = (what, ...args) => wr[what].addListener(listeners[what], ...args);
|
||||
let allUrls = ["<all_urls>"];
|
||||
let docTypes = ["main_frame", "sub_frame", "object"];
|
||||
let filterDocs = {urls: allUrls, types: docTypes};
|
||||
let filterAll = {urls: allUrls};
|
||||
listen("onBeforeRequest", filterAll, ["blocking"]);
|
||||
const wr = browser.webRequest;
|
||||
const listen = (what, ...args) => wr[what].addListener(listeners[what], ...args);
|
||||
const allUrls = ["<all_urls>"];
|
||||
const docTypes = ["main_frame", "sub_frame", "object"];
|
||||
const filterDocs = {urls: allUrls, types: docTypes};
|
||||
const filterAll = {urls: allUrls};
|
||||
|
||||
listen("onBeforeRequest", filterAll,
|
||||
RequestGuard.canBlock ? ["blocking"] : []);
|
||||
listen("onResponseStarted", filterDocs, ["responseHeaders"]);
|
||||
listen("onCompleted", filterAll);
|
||||
listen("onErrorOccurred", filterAll);
|
||||
DocStartInjection.register(injectPolicyScript);
|
||||
TabStatus.probe();
|
||||
|
||||
if (!RequestGuard.canBlock) {
|
||||
include("/bg/DNRPolicy.js")
|
||||
} else {
|
||||
// From here on, only webRequestBlocking-enabled code (Gecko MV2.5)
|
||||
listen("onBeforeSendHeaders", filterAll, ["blocking", "requestHeaders"]);
|
||||
|
||||
let mergingCSP = "getBrowserInfo" in browser.runtime;
|
||||
if (mergingCSP) {
|
||||
let {vendor, version} = await browser.runtime.getBrowserInfo();
|
||||
mergingCSP = vendor === "Mozilla" && parseInt(version) >= 77;
|
||||
}
|
||||
const mergingCSP = true; // TODO: check whether it's still true...
|
||||
if (mergingCSP) {
|
||||
// In Gecko>=77 (https://bugzilla.mozilla.org/show_bug.cgi?id=1462989)
|
||||
// we need to cleanup our own cached headers in a dedicated listener :(
|
||||
|
@ -848,33 +896,6 @@ var RequestGuard = (() => {
|
|||
}
|
||||
return ALLOW;
|
||||
}, filterDocs, ["blocking", "responseHeaders"])).install();
|
||||
|
||||
listen("onResponseStarted", filterDocs, ["responseHeaders"]);
|
||||
listen("onCompleted", filterAll);
|
||||
listen("onErrorOccurred", filterAll);
|
||||
if (csp.reportURI) {
|
||||
wr.onBeforeRequest.addListener(onViolationReport,
|
||||
{urls: [csp.reportURI], types: ["csp_report"]}, ["blocking", "requestBody"]);
|
||||
}
|
||||
DocStartInjection.register(injectPolicyScript);
|
||||
TabStatus.probe();
|
||||
},
|
||||
stop() {
|
||||
let wr = browser.webRequest;
|
||||
for (let [name, listener] of Object.entries(listeners)) {
|
||||
if (typeof listener === "function") {
|
||||
wr[name].removeListener(listener);
|
||||
} else if (listener instanceof LastListener) {
|
||||
listener.uninstall();
|
||||
}
|
||||
}
|
||||
wr.onBeforeRequest.removeListener(onViolationReport);
|
||||
if (listeners.onHeadersReceived.resetCSP) {
|
||||
wr.onHeadersReceived.removeListener(listeners.onHeadersReceived.resetCSP);
|
||||
}
|
||||
DocStartInjection.unregister(injectPolicyScript);
|
||||
Messages.removeHandler(messageHandler);
|
||||
}
|
||||
};
|
||||
return RequestGuard;
|
||||
})();
|
||||
}
|
|
@ -163,7 +163,7 @@ var Settings = {
|
|||
reloadOptionsUI = true;
|
||||
}
|
||||
|
||||
await Promise.all(["local", "sync"].map(
|
||||
await Promise.allSettled(["local", "sync"].map(
|
||||
async storage => (settings[storage] || // changed or...
|
||||
settings[storage] === null // ... needs reset to default
|
||||
) && await ns.save(settings[storage]
|
||||
|
@ -180,7 +180,7 @@ var Settings = {
|
|||
}
|
||||
|
||||
if (typeof unrestrictedTab === "boolean") {
|
||||
ns.toggleTabRestrictions(tabId, !unrestrictedTab);
|
||||
await ns.toggleTabRestrictions(tabId, !unrestrictedTab);
|
||||
}
|
||||
if (reloadAffected && tabId !== -1) {
|
||||
try {
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// depends on /nscl/service/Scripting.js
|
||||
// depends on /nscl/common/SessionCache.js
|
||||
// depends on /nscl/service/TabCache.js
|
||||
// depends on /nscl/service/TabTies.js
|
||||
|
@ -111,6 +112,7 @@ var TabGuard = (() => {
|
|||
wakening: Promise.all([TabCache.wakening, TabTies.wakening, session.load()]),
|
||||
forget,
|
||||
// must be called from a webRequest.onBeforeSendHeaders blocking listener
|
||||
// TODO: explore DNR alternative
|
||||
onSend(request) {
|
||||
const mode = ns.sync.TabGuardMode;
|
||||
if (mode === "off" || !request.incognito && mode!== "global") return;
|
||||
|
@ -189,16 +191,18 @@ var TabGuard = (() => {
|
|||
return suspiciousTabs.length > 0 && (async () => {
|
||||
|
||||
let suspiciousDomains = [];
|
||||
await Promise.all(suspiciousTabs.map(async (tab) => {
|
||||
await Promise.allSettled(suspiciousTabs.map(async (tab) => {
|
||||
if (!tab._isExplicitOrigin) { // e.g. about:blank
|
||||
// let's try retrieving actual origin
|
||||
tab._externalUrl = tab.url;
|
||||
tab._isExplicitOrigin = true;
|
||||
try {
|
||||
tab.url = await browser.tabs.executeScript(tab.id, {
|
||||
runAt: "document_start",
|
||||
code: "window.origin === 'null' ? window.location.href : window.origin"
|
||||
});
|
||||
tab.url = (await Scripting.executeScript({
|
||||
target: {tabId: tab.id, allFrames: false},
|
||||
func: () => {
|
||||
return window.origin === 'null' ? window.location.href : window.origin;
|
||||
},
|
||||
}))[0].result;
|
||||
} catch (e) {
|
||||
// We don't have permissions to run in this tab, probably because it has been left empty.
|
||||
debug(e);
|
||||
|
@ -223,10 +227,10 @@ var TabGuard = (() => {
|
|||
}
|
||||
if (!tab._contentType) {
|
||||
try {
|
||||
tab._contentType = await browser.tabs.executeScript(tab.id, {
|
||||
runAt: "document_start",
|
||||
code: "document.contentType"
|
||||
});
|
||||
tab._contentType = (await Scripting.executeScript({
|
||||
target: {tabId: tab.id},
|
||||
func() { return document.contentType }
|
||||
}))[0].result;
|
||||
} catch (e) {
|
||||
// We don't have permissions to run in this tab: privileged!
|
||||
debug(e);
|
||||
|
@ -292,6 +296,7 @@ var TabGuard = (() => {
|
|||
})();
|
||||
},
|
||||
// must be called from a webRequest.onHeadersReceived blocking listener
|
||||
// TODO: explore DNR alternative
|
||||
onReceive(request) {
|
||||
if (!anonymizedRequests.has(request.id)) return false;
|
||||
let headersModified = false;
|
||||
|
|
|
@ -18,7 +18,9 @@
|
|||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
{
|
||||
// depends on /nscl/service/Scripting.js
|
||||
|
||||
{
|
||||
'use strict';
|
||||
{
|
||||
for (let event of ["onInstalled", "onUpdateAvailable"]) {
|
||||
|
@ -36,7 +38,7 @@
|
|||
const menuUpdater = async (tabId) => {
|
||||
if (!menuShowing) return;
|
||||
try {
|
||||
const badgeText = await browser.browserAction.getBadgeText({tabId});
|
||||
const badgeText = await browser.action.getBadgeText({tabId});
|
||||
let title = "NoScript";
|
||||
if (badgeText) title = `${title} [${badgeText}]`;
|
||||
await browser.contextMenus.update(ctxMenuId, {title});
|
||||
|
@ -69,9 +71,9 @@
|
|||
{
|
||||
afterLoad(data) {
|
||||
if (data) {
|
||||
ns.policy = new Policy(data.policy);
|
||||
ns.unrestrictedTabs = new Set(data.unrestrictedTabs);
|
||||
ns.gotTorBrowserInit = data.gotTorBrowserInit;
|
||||
ns.unrestrictedTabs = new Set(data.unrestrictedTabs);
|
||||
ns.policy = new Policy(data.policy);
|
||||
}
|
||||
},
|
||||
beforeSave() { // beforeSave
|
||||
|
@ -90,10 +92,12 @@
|
|||
if (!ns.policy) { // ns.policy could have been already set by LifeCycle or SessionCache
|
||||
const policyData = (await Storage.get("sync", "policy")).policy;
|
||||
if (policyData && policyData.DEFAULT) {
|
||||
ns.policy = new Policy(policyData);
|
||||
if (ns.local.enforceOnRestart && !ns.policy.enforced) {
|
||||
ns.policy.enforced = true;
|
||||
const policy = new Policy(policyData);
|
||||
if (ns.local.enforceOnRestart && !policy.enforced) {
|
||||
(ns.policy = policy).enforced = true;
|
||||
await ns.savePolicy();
|
||||
} else {
|
||||
ns.policy = policy;
|
||||
}
|
||||
} else {
|
||||
ns.policy = new Policy(Settings.createDefaultDryPolicy());
|
||||
|
@ -108,7 +112,6 @@
|
|||
await include("/nscl/service/prefetchCSSResources.js");
|
||||
}
|
||||
await TabGuard.wakening;
|
||||
await RequestGuard.start();
|
||||
|
||||
try {
|
||||
await Messages.send("started");
|
||||
|
@ -130,7 +133,7 @@
|
|||
active: true
|
||||
}));
|
||||
if (tab) {
|
||||
ns.toggleTabRestrictions(tab.id);
|
||||
await ns.toggleTabRestrictions(tab.id);
|
||||
browser.tabs.reload(tab.id);
|
||||
}
|
||||
},
|
||||
|
@ -154,7 +157,7 @@
|
|||
}
|
||||
|
||||
// wiring main UI
|
||||
browser.browserAction.setPopup({popup: popupURL});
|
||||
browser.action.setPopup({popup: popupURL});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -222,29 +225,30 @@
|
|||
});
|
||||
},
|
||||
async getTheme(msg, {tab, frameId}) {
|
||||
let code = await Themes.getContentCSS();
|
||||
let css = await Themes.getContentCSS();
|
||||
if (!ns.local.showProbePlaceholders) {
|
||||
code += `\n.__NoScript_Offscreen_PlaceHolders__ {display: none}`;
|
||||
css += `\n.__NoScript_Offscreen_PlaceHolders__ {display: none}`;
|
||||
}
|
||||
try {
|
||||
browser.tabs.insertCSS(tab.id, {
|
||||
code,
|
||||
frameId,
|
||||
runAt: "document_start",
|
||||
matchAboutBlank: true,
|
||||
cssOrigin: "user",
|
||||
await Scripting.insertCSS({
|
||||
target: {tabId: tab.id, frameId},
|
||||
css,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return {"vintage": await Themes.isVintage()};
|
||||
const ret = {"vintage": await Themes.isVintage()};
|
||||
console.debug("Returning from getTheme", ret); // DEV_ONLY
|
||||
return ret;
|
||||
},
|
||||
|
||||
async promptHook(msg, {tabId}) {
|
||||
await browser.tabs.executeScript(tabId, {
|
||||
code: "try { if (document.fullscreenElement) document.exitFullscreen(); } catch (e) {}",
|
||||
matchAboutBlank: true,
|
||||
allFrames: true,
|
||||
const func = () => {
|
||||
try { if (document.fullscreenElement) document.exitFullscreen(); } catch (e) {}
|
||||
};
|
||||
await Scripting.executeScript({
|
||||
target: {tabId},
|
||||
func,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -265,16 +269,24 @@
|
|||
}
|
||||
}
|
||||
|
||||
var ns = {
|
||||
let _policy = null;
|
||||
|
||||
globalThis.ns = {
|
||||
running: false,
|
||||
policy: null,
|
||||
set policy(p) {
|
||||
_policy = p;
|
||||
RequestGuard.DNRPolicy?.update();
|
||||
},
|
||||
get policy() { return _policy; },
|
||||
local: null,
|
||||
sync: null,
|
||||
initializing: null,
|
||||
unrestrictedTabs: new Set(),
|
||||
toggleTabRestrictions(tabId, restrict = ns.unrestrictedTabs.has(tabId)) {
|
||||
async toggleTabRestrictions(tabId, restrict = ns.unrestrictedTabs.has(tabId)) {
|
||||
ns.unrestrictedTabs[restrict ? "delete": "add"](tabId);
|
||||
session.save();
|
||||
Promise.allSettled([session.save(),
|
||||
RequestGuard.DNRPolicy?.updateTabs()
|
||||
]);
|
||||
},
|
||||
isEnforced(tabId = -1) {
|
||||
return this.policy.enforced && (tabId === -1 || !this.unrestrictedTabs.has(tabId));
|
||||
|
@ -342,7 +354,8 @@
|
|||
|
||||
async init() {
|
||||
browser.runtime.onSyncMessage.addListener(onSyncMessage);
|
||||
await Wakening.waitFor(Messages.wakening = this.initializing = init());
|
||||
await (Messages.wakening = this.initializing = init());
|
||||
Wakening.done();
|
||||
Commands.install();
|
||||
try {
|
||||
this.devMode = (await browser.management.getSelf()).installType === "development";
|
||||
|
@ -369,7 +382,7 @@
|
|||
|
||||
async savePolicy() {
|
||||
if (this.policy) {
|
||||
await Promise.all([
|
||||
await Promise.allSettled([
|
||||
Storage.set("sync", {
|
||||
policy: this.policy.dry()
|
||||
}),
|
||||
|
|
|
@ -28,6 +28,10 @@ a.__NoScript_PlaceHolder__ {
|
|||
border-radius: 4px;
|
||||
}
|
||||
|
||||
a.__NoScript_PlaceHolder__.no-theme {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
a.__NoScript_PlaceHolder__.mozilla {
|
||||
background-image: var(--img-logo);
|
||||
}
|
||||
|
|
|
@ -126,21 +126,29 @@ var notifyPage = async () => {
|
|||
|
||||
window.addEventListener("pageshow", notifyPage);
|
||||
|
||||
let violations = new Set();
|
||||
let documentOrigin = new URL(document.URL).origin;
|
||||
window.addEventListener("securitypolicyviolation", e => {
|
||||
const violations = new Set();
|
||||
const documentOrigin = new URL(document.URL).origin;
|
||||
|
||||
window.addEventListener("securitypolicyviolation", async e => {
|
||||
if (!e.isTrusted) return;
|
||||
let {violatedDirective, originalPolicy} = e;
|
||||
let {violatedDirective, originalPolicy, disposition} = e;
|
||||
|
||||
let type = violatedDirective.split("-", 1)[0]; // e.g. script-src 'none' => script
|
||||
let url = e.blockedURI;
|
||||
if (type === "media" && /^data\b/.test(url) && (!CSP.isMediaBlocker(originalPolicy) ||
|
||||
ns.embeddingDocument || !document.querySelector("video,audio")))
|
||||
ns.embeddingDocument || !document.querySelector("video,audio"))) {
|
||||
// MediaBlocker probe, don't report
|
||||
return;
|
||||
if (!ns.CSP || !(CSP.normalize(originalPolicy).includes(ns.CSP))) {
|
||||
}
|
||||
|
||||
const isReport = disposition === "report" &&
|
||||
/; report-to noscript-reports-[\w-]+$/.test(originalPolicy);
|
||||
if (!(isReport ||
|
||||
ns.CSP && CSP.normalize(originalPolicy).includes(ns.CSP))) {
|
||||
// this seems to come from page's own CSP
|
||||
return;
|
||||
}
|
||||
|
||||
let documentUrl = document.URL;
|
||||
let origin;
|
||||
if (!(url && url.includes(":"))) {
|
||||
|
@ -149,23 +157,55 @@ window.addEventListener("securitypolicyviolation", e => {
|
|||
} else {
|
||||
({origin} = new URL(url));
|
||||
}
|
||||
let key = RequestKey.create(origin, type, documentOrigin);
|
||||
const key = RequestKey.create(url, type, documentOrigin);
|
||||
if (violations.has(key)) return;
|
||||
violations.add(key);
|
||||
if (type === "frame") type = "sub_frame";
|
||||
seen.record({
|
||||
policyType: type,
|
||||
request: {
|
||||
key,
|
||||
url,
|
||||
type,
|
||||
documentUrl,
|
||||
},
|
||||
allowed: false
|
||||
});
|
||||
Messages.send("violation", {url, type});
|
||||
Messages.send("violation", {url, type, isReport});
|
||||
}, true);
|
||||
|
||||
if (!/^https:/.test(location.protocol)) {
|
||||
// Reporting CSP can only be injected in HTTP responses,
|
||||
// let's emulate them using mutation observers
|
||||
const checked = new Set();
|
||||
const checkSrc = async (node) => {
|
||||
if (!('src' in node && node.parentNode)) {
|
||||
return;
|
||||
}
|
||||
const type = node instanceof HTMLMediaElement ? "media"
|
||||
: node instanceof HTMLIFrameElement ? "sub_frame"
|
||||
: node instanceof HTMLObjectElement || node instanceof HTMLEmbedElement ? "object"
|
||||
: "";
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
const url = node.src;
|
||||
const key = RequestKey.create(url, type, documentOrigin);
|
||||
if (checked.has(key)) {
|
||||
return;
|
||||
}
|
||||
checked.add(key);
|
||||
Messages.send("violation", {url, type, isReport: true});
|
||||
}
|
||||
const mutationsCallback = records => {
|
||||
for (var r of records) {
|
||||
switch (r.type) {
|
||||
case "attributes":
|
||||
checkSrc(r.target);
|
||||
break;
|
||||
case "childList":
|
||||
[...r.addedNodes].forEach(checkSrc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
const observer = new MutationObserver(mutationsCallback);
|
||||
observer.observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributeFilter: ["src"],
|
||||
});
|
||||
}
|
||||
|
||||
ns.on("capabilities", () => {
|
||||
seen.record({
|
||||
|
|
|
@ -172,6 +172,11 @@
|
|||
return this.capabilities && this.capabilities.has(cap);
|
||||
},
|
||||
};
|
||||
window.ns = window.ns ? Object.assign(ns, window.ns) : ns;
|
||||
debug("StaticNS", Date.now(), JSON.stringify(window.ns)); // DEV_ONLY
|
||||
globalThis.ns = globalThis.ns ? Object.assign(ns, globalThis.ns) : ns;
|
||||
debug("StaticNS", Date.now(), JSON.stringify(globalThis.ns)); // DEV_ONLY
|
||||
globalThis.ns_setupCallBack = ns.domPolicy
|
||||
? () => {}
|
||||
: ({domPolicy}) => {
|
||||
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,8 +14,6 @@
|
|||
"description": "__MSG_Description__",
|
||||
"incognito": "spanning",
|
||||
|
||||
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'none'",
|
||||
|
||||
"icons": {
|
||||
"48": "img/icon48.png",
|
||||
"96": "img/icon96.png",
|
||||
|
@ -27,15 +25,20 @@
|
|||
"storage",
|
||||
"tabs",
|
||||
"unlimitedStorage",
|
||||
"scripting",
|
||||
"declarativeNetRequest",
|
||||
"webNavigation",
|
||||
"webRequest",
|
||||
"webRequestBlocking",
|
||||
"dns",
|
||||
"<all_urls>"
|
||||
],
|
||||
"host_permissions": [
|
||||
"<all_urls>"
|
||||
],
|
||||
|
||||
"background": {
|
||||
"persistent": false,
|
||||
"service_worker": "sw.js",
|
||||
"scripts": [
|
||||
"/nscl/lib/browser-polyfill.js",
|
||||
"/nscl/lib/punycode.js",
|
||||
|
@ -61,6 +64,7 @@
|
|||
"/nscl/common/AddressMatcherWithDNS.js",
|
||||
"/nscl/common/iputil.js",
|
||||
"/nscl/common/SessionCache.js",
|
||||
"/nscl/service/Scripting.js",
|
||||
"/nscl/service/DocStartInjection.js",
|
||||
"/nscl/service/LastListener.js",
|
||||
"/nscl/service/patchWorkers.js",
|
||||
|
@ -75,7 +79,8 @@
|
|||
"bg/Settings.js",
|
||||
"bg/main.js",
|
||||
"common/themes.js"
|
||||
]
|
||||
],
|
||||
"persistent": false
|
||||
},
|
||||
|
||||
"content_scripts": [
|
||||
|
@ -119,7 +124,6 @@
|
|||
"match_origin_as_fallback": true,
|
||||
"all_frames": true,
|
||||
"js": [
|
||||
"/nscl/common/UA.js",
|
||||
"content/ftp.js",
|
||||
"/nscl/content/DocumentFreezer.js",
|
||||
"content/syncFetchPolicy.js"
|
||||
|
@ -132,6 +136,14 @@
|
|||
"open_in_tab": true
|
||||
},
|
||||
|
||||
"action": {
|
||||
"default_area": "navbar",
|
||||
"default_title": "NoScript",
|
||||
"default_icon": {
|
||||
"64": "img/ui-maybe64.png"
|
||||
}
|
||||
},
|
||||
|
||||
"browser_action": {
|
||||
"default_area": "navbar",
|
||||
"default_title": "NoScript",
|
||||
|
@ -141,12 +153,6 @@
|
|||
},
|
||||
|
||||
"commands": {
|
||||
"openPageUI": {
|
||||
"description": "__MSG_pagePermissionsUI__",
|
||||
"suggested_key": {
|
||||
"default": "Alt+Shift+N"
|
||||
}
|
||||
},
|
||||
"toggleEnforcementForTab": {
|
||||
"description": "__MSG_toggleEnforcementForTab__",
|
||||
"suggested_key": {
|
||||
|
@ -154,6 +160,14 @@
|
|||
"windows": "Alt+Shift+Comma"
|
||||
}
|
||||
},
|
||||
|
||||
"openPageUI": {
|
||||
"description": "__MSG_pagePermissionsUI__",
|
||||
"suggested_key": {
|
||||
"default": "Alt+Shift+N"
|
||||
}
|
||||
},
|
||||
|
||||
"tempTrustPage": {
|
||||
"description": "__MSG_TempTrustPage__"
|
||||
},
|
||||
|
@ -161,6 +175,7 @@
|
|||
"description": "__MSG_RevokeTemp__"
|
||||
},
|
||||
|
||||
"_execute_action": {},
|
||||
"_execute_browser_action": {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* NoScript - a Firefox extension for whitelist driven safe JavaScript execution
|
||||
*
|
||||
* Copyright (C) 2005-2024 Giorgio Maone <https://maone.net>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by the Free Software
|
||||
* Foundation, either version 3 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
importScripts("/nscl/common/include.js");
|
||||
browser.browserAction = browser.action;
|
||||
console.log("NoScript Manifest V3 service worker");
|
|
@ -156,7 +156,7 @@ document.querySelector("#version").textContent = _("Version",
|
|||
opt("showFullAddresses", "local");
|
||||
opt("showProbePlaceholders", "local");
|
||||
|
||||
UI.wireChoice("theme", o => Themes.setup(o && o.value) );
|
||||
UI.wireChoice("theme", async o => await Themes.setup(o?.value) );
|
||||
opt("vintageTheme", async o => await (o ? Themes.setVintage(o.checked) : Themes.isVintage()));
|
||||
addEventListener("NoScriptThemeChanged", ({detail}) => {
|
||||
if ("theme" in detail) {
|
||||
|
|
|
@ -249,13 +249,16 @@ addEventListener("unload", e => {
|
|||
|
||||
setupEnforcement();
|
||||
|
||||
|
||||
let mainFrame = UI.seen && UI.seen.find(thing => thing.request.type === "main_frame");
|
||||
debug("Seen: %o", UI.seen);
|
||||
if (!mainFrame) {
|
||||
let isHttp = /^https?:/.test(pageTab.url);
|
||||
try {
|
||||
await browser.tabs.executeScript(tabId, { code: "" });
|
||||
await include("/nscl/service/Scripting.js");
|
||||
await Scripting.executeScript({
|
||||
target: {tabId, allFrames: false},
|
||||
func: () => {}
|
||||
});
|
||||
if (isHttp) {
|
||||
document.body.classList.add("disabled");
|
||||
messageBox("warning", _("freshInstallReload"));
|
||||
|
|
|
@ -39,12 +39,12 @@ var XSS = (() => {
|
|||
async function getUserResponse(xssReq) {
|
||||
let {originKey, request} = xssReq;
|
||||
let {tabId, frameId} = request;
|
||||
let {browserAction} = browser;
|
||||
const {action} = browser;
|
||||
if (frameId === 0) {
|
||||
if (blockedTabs.has(tabId)) {
|
||||
blockedTabs.delete(tabId);
|
||||
if ("setBadgeText" in browserAction) {
|
||||
browserAction.setBadgeText({tabId, text: ""});
|
||||
if ("setBadgeText" in action) {
|
||||
action.setBadgeText({tabId, text: ""});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -57,9 +57,9 @@ var XSS = (() => {
|
|||
log("Blocking request from %s to %s by previous XSS prompt user choice",
|
||||
xssReq.srcUrl, xssReq.destUrl);
|
||||
|
||||
if ("setBadgeText" in browserAction) {
|
||||
browserAction.setBadgeText({tabId, text: "XSS"});
|
||||
browserAction.setBadgeBackgroundColor({tabId, color: [128, 0, 0, 160]});
|
||||
if ("setBadgeText" in action) {
|
||||
action.setBadgeText({tabId, text: "XSS"});
|
||||
action.setBadgeBackgroundColor({tabId, color: [128, 0, 0, 160]});
|
||||
}
|
||||
let keys = blockedTabs.get(tabId);
|
||||
if (!keys) blockedTabs.set(tabId, keys = new Set());
|
||||
|
|
Loading…
Reference in New Issue