summaryrefslogtreecommitdiffstats
path: root/browser/extensions/webcompat/lib/shims.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/extensions/webcompat/lib/shims.js')
-rw-r--r--browser/extensions/webcompat/lib/shims.js1044
1 files changed, 1044 insertions, 0 deletions
diff --git a/browser/extensions/webcompat/lib/shims.js b/browser/extensions/webcompat/lib/shims.js
new file mode 100644
index 0000000000..ee33627c57
--- /dev/null
+++ b/browser/extensions/webcompat/lib/shims.js
@@ -0,0 +1,1044 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* globals browser, module, onMessageFromTab */
+
+// To grant shims access to bundled logo images without risking
+// exposing our moz-extension URL, we have the shim request them via
+// nonsense URLs which we then redirect to the actual files (but only
+// on tabs where a shim using a given logo happens to be active).
+const LogosBaseURL = "https://smartblock.firefox.etp/";
+
+const releaseBranchPromise = browser.appConstants.getReleaseBranch();
+
+const platformPromise = browser.runtime.getPlatformInfo().then(info => {
+ return info.os === "android" ? "android" : "desktop";
+});
+
+let debug = async function () {
+ if ((await releaseBranchPromise) !== "release_or_beta") {
+ console.debug.apply(this, arguments);
+ }
+};
+let error = async function () {
+ if ((await releaseBranchPromise) !== "release_or_beta") {
+ console.error.apply(this, arguments);
+ }
+};
+let warn = async function () {
+ if ((await releaseBranchPromise) !== "release_or_beta") {
+ console.warn.apply(this, arguments);
+ }
+};
+
+class Shim {
+ constructor(opts, manager) {
+ this.manager = manager;
+
+ const { contentScripts, matches, unblocksOnOptIn } = opts;
+
+ this.branches = opts.branches;
+ this.bug = opts.bug;
+ this.isGoogleTrendsDFPIFix = opts.custom == "google-trends-dfpi-fix";
+ this.file = opts.file;
+ this.hiddenInAboutCompat = opts.hiddenInAboutCompat;
+ this.hosts = opts.hosts;
+ this.id = opts.id;
+ this.logos = opts.logos || [];
+ this.matches = [];
+ this.name = opts.name;
+ this.notHosts = opts.notHosts;
+ this.onlyIfBlockedByETP = opts.onlyIfBlockedByETP;
+ this.onlyIfDFPIActive = opts.onlyIfDFPIActive;
+ this.onlyIfPrivateBrowsing = opts.onlyIfPrivateBrowsing;
+ this._options = opts.options || {};
+ this.needsShimHelpers = opts.needsShimHelpers;
+ this.platform = opts.platform || "all";
+ this.runFirst = opts.runFirst;
+ this.unblocksOnOptIn = unblocksOnOptIn;
+ this.requestStorageAccessForRedirect = opts.requestStorageAccessForRedirect;
+
+ this._hostOptIns = new Set();
+
+ this._disabledByConfig = opts.disabled;
+ this._disabledGlobally = false;
+ this._disabledForSession = false;
+ this._disabledByPlatform = false;
+ this._disabledByReleaseBranch = false;
+
+ this._activeOnTabs = new Set();
+ this._showedOptInOnTabs = new Set();
+
+ const pref = `disabled_shims.${this.id}`;
+
+ this.redirectsRequests = !!this.file && matches?.length;
+
+ this._contentScriptRegistrations = [];
+ this.contentScripts = contentScripts || [];
+ for (const script of this.contentScripts) {
+ if (typeof script.css === "string") {
+ script.css = [{ file: `/shims/${script.css}` }];
+ }
+ if (typeof script.js === "string") {
+ script.js = [{ file: `/shims/${script.js}` }];
+ }
+ }
+
+ for (const match of matches || []) {
+ if (!match.types) {
+ this.matches.push({ patterns: [match], types: ["script"] });
+ } else {
+ this.matches.push(match);
+ }
+ if (match.target) {
+ this.redirectsRequests = true;
+ }
+ }
+
+ browser.aboutConfigPrefs.onPrefChange.addListener(async () => {
+ const value = await browser.aboutConfigPrefs.getPref(pref);
+ this._disabledPrefValue = value;
+ this._onEnabledStateChanged();
+ }, pref);
+
+ this.ready = Promise.all([
+ browser.aboutConfigPrefs.getPref(pref),
+ platformPromise,
+ releaseBranchPromise,
+ ]).then(([disabledPrefValue, platform, branch]) => {
+ this._disabledPrefValue = disabledPrefValue;
+
+ this._disabledByPlatform =
+ this.platform !== "all" && this.platform !== platform;
+
+ this._disabledByReleaseBranch = false;
+ for (const supportedBranchAndPlatform of this.branches || []) {
+ const [supportedBranch, supportedPlatform] =
+ supportedBranchAndPlatform.split(":");
+ if (
+ (!supportedPlatform || supportedPlatform == platform) &&
+ supportedBranch != branch
+ ) {
+ this._disabledByReleaseBranch = true;
+ }
+ }
+
+ this._preprocessOptions(platform, branch);
+ this._onEnabledStateChanged();
+ });
+ }
+
+ _preprocessOptions(platform, branch) {
+ // options may be any value, but can optionally be gated for specified
+ // platform/branches, if in the format `{value, branches, platform}`
+ this.options = {};
+ for (const [k, v] of Object.entries(this._options)) {
+ if (v?.value) {
+ if (
+ (!v.platform || v.platform === platform) &&
+ (!v.branches || v.branches.includes(branch))
+ ) {
+ this.options[k] = v.value;
+ }
+ } else {
+ this.options[k] = v;
+ }
+ }
+ }
+
+ get enabled() {
+ if (this._disabledGlobally || this._disabledForSession) {
+ return false;
+ }
+
+ if (this._disabledPrefValue !== undefined) {
+ return !this._disabledPrefValue;
+ }
+
+ return (
+ !this._disabledByConfig &&
+ !this._disabledByPlatform &&
+ !this._disabledByReleaseBranch
+ );
+ }
+
+ get disabledReason() {
+ if (this._disabledGlobally) {
+ return "globalPref";
+ }
+
+ if (this._disabledForSession) {
+ return "session";
+ }
+
+ if (this._disabledPrefValue !== undefined) {
+ if (this._disabledPrefValue === true) {
+ return "pref";
+ }
+ return false;
+ }
+
+ if (this._disabledByConfig) {
+ return "config";
+ }
+
+ if (this._disabledByPlatform) {
+ return "platform";
+ }
+
+ if (this._disabledByReleaseBranch) {
+ return "releaseBranch";
+ }
+
+ return false;
+ }
+
+ onAllShimsEnabled() {
+ const wasEnabled = this.enabled;
+ this._disabledGlobally = false;
+ if (!wasEnabled) {
+ this._onEnabledStateChanged();
+ }
+ }
+
+ onAllShimsDisabled() {
+ const wasEnabled = this.enabled;
+ this._disabledGlobally = true;
+ if (wasEnabled) {
+ this._onEnabledStateChanged();
+ }
+ }
+
+ enableForSession() {
+ const wasEnabled = this.enabled;
+ this._disabledForSession = false;
+ if (!wasEnabled) {
+ this._onEnabledStateChanged();
+ }
+ }
+
+ disableForSession() {
+ const wasEnabled = this.enabled;
+ this._disabledForSession = true;
+ if (wasEnabled) {
+ this._onEnabledStateChanged();
+ }
+ }
+
+ async _onEnabledStateChanged() {
+ this.manager?.onShimStateChanged(this.id);
+ if (!this.enabled) {
+ await this._unregisterContentScripts();
+ return this._revokeRequestsInETP();
+ }
+ await this._registerContentScripts();
+ return this._allowRequestsInETP();
+ }
+
+ async _registerContentScripts() {
+ if (
+ this.contentScripts.length &&
+ !this._contentScriptRegistrations.length
+ ) {
+ const matches = [];
+ for (const options of this.contentScripts) {
+ matches.push(options.matches);
+ const reg = await browser.contentScripts.register(options);
+ this._contentScriptRegistrations.push(reg);
+ }
+ const urls = Array.from(new Set(matches.flat()));
+ debug("Enabling content scripts for these URLs:", urls);
+ }
+ }
+
+ async _unregisterContentScripts() {
+ for (const registration of this._contentScriptRegistrations) {
+ registration.unregister();
+ }
+ this._contentScriptRegistrations = [];
+ }
+
+ async _allowRequestsInETP() {
+ const matches = this.matches.map(m => m.patterns).flat();
+ if (matches.length) {
+ await browser.trackingProtection.shim(this.id, matches);
+ }
+
+ if (this._hostOptIns.size) {
+ const optIns = this.getApplicableOptIns();
+ if (optIns.length) {
+ await browser.trackingProtection.allow(
+ this.id,
+ this._optInPatterns,
+ Array.from(this._hostOptIns)
+ );
+ }
+ }
+ }
+
+ _revokeRequestsInETP() {
+ return browser.trackingProtection.revoke(this.id);
+ }
+
+ setActiveOnTab(tabId, active = true) {
+ if (active) {
+ this._activeOnTabs.add(tabId);
+ } else {
+ this._activeOnTabs.delete(tabId);
+ this._showedOptInOnTabs.delete(tabId);
+ }
+ }
+
+ isActiveOnTab(tabId) {
+ return this._activeOnTabs.has(tabId);
+ }
+
+ meantForHost(host) {
+ const { hosts, notHosts } = this;
+ if (hosts || notHosts) {
+ if (
+ (notHosts && notHosts.includes(host)) ||
+ (hosts && !hosts.includes(host))
+ ) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ async unblocksURLOnOptIn(url) {
+ if (!this._optInPatterns) {
+ this._optInPatterns = await this.getApplicableOptIns();
+ }
+
+ if (!this._optInMatcher) {
+ this._optInMatcher = browser.matchPatterns.getMatcher(
+ Array.from(this._optInPatterns)
+ );
+ }
+
+ return this._optInMatcher.matches(url);
+ }
+
+ isTriggeredByURLAndType(url, type) {
+ for (const entry of this.matches || []) {
+ if (!entry.types.includes(type)) {
+ continue;
+ }
+ if (!entry.matcher) {
+ entry.matcher = browser.matchPatterns.getMatcher(
+ Array.from(entry.patterns)
+ );
+ }
+ if (entry.matcher.matches(url)) {
+ return entry;
+ }
+ }
+
+ return undefined;
+ }
+
+ async getApplicableOptIns() {
+ if (this._applicableOptIns) {
+ return this._applicableOptIns;
+ }
+ const optins = [];
+ for (const unblock of this.unblocksOnOptIn || []) {
+ if (typeof unblock === "string") {
+ optins.push(unblock);
+ continue;
+ }
+ const { branches, patterns, platforms } = unblock;
+ if (platforms?.length) {
+ const platform = await platformPromise;
+ if (platform !== "all" && !platforms.includes(platform)) {
+ continue;
+ }
+ }
+ if (branches?.length) {
+ const branch = await releaseBranchPromise;
+ if (!branches.includes(branch)) {
+ continue;
+ }
+ }
+ optins.push.apply(optins, patterns);
+ }
+ this._applicableOptIns = optins;
+ return optins;
+ }
+
+ async onUserOptIn(host) {
+ const optins = await this.getApplicableOptIns();
+ if (optins.length) {
+ this.userHasOptedIn = true;
+ this._hostOptIns.add(host);
+ await browser.trackingProtection.allow(
+ this.id,
+ optins,
+ Array.from(this._hostOptIns)
+ );
+ }
+ }
+
+ hasUserOptedInAlready(host) {
+ return this._hostOptIns.has(host);
+ }
+
+ showOptInWarningOnce(tabId, origin) {
+ if (this._showedOptInOnTabs.has(tabId)) {
+ return Promise.resolve();
+ }
+ this._showedOptInOnTabs.add(tabId);
+
+ const { bug, name } = this;
+ const warning = `${name} is allowed on ${origin} for this browsing session due to user opt-in. See https://bugzilla.mozilla.org/show_bug.cgi?id=${bug} for details.`;
+ return browser.tabs
+ .executeScript(tabId, {
+ code: `console.warn(${JSON.stringify(warning)})`,
+ runAt: "document_start",
+ })
+ .catch(() => {});
+ }
+}
+
+class Shims {
+ constructor(availableShims) {
+ if (!browser.trackingProtection) {
+ console.error("Required experimental add-on APIs for shims unavailable");
+ return;
+ }
+
+ this._registerShims(availableShims);
+
+ onMessageFromTab(this._onMessageFromShim.bind(this));
+
+ this.ENABLED_PREF = "enable_shims";
+ browser.aboutConfigPrefs.onPrefChange.addListener(() => {
+ this._checkEnabledPref();
+ }, this.ENABLED_PREF);
+ this._haveCheckedEnabledPref = this._checkEnabledPref();
+ }
+
+ bindAboutCompatBroker(broker) {
+ this._aboutCompatBroker = broker;
+ }
+
+ getShimInfoForAboutCompat(shim) {
+ const { bug, disabledReason, hiddenInAboutCompat, id, name } = shim;
+ const type = "smartblock";
+ return { bug, disabledReason, hidden: hiddenInAboutCompat, id, name, type };
+ }
+
+ disableShimForSession(id) {
+ const shim = this.shims.get(id);
+ shim?.disableForSession();
+ }
+
+ enableShimForSession(id) {
+ const shim = this.shims.get(id);
+ shim?.enableForSession();
+ }
+
+ onShimStateChanged(id) {
+ if (!this._aboutCompatBroker) {
+ return;
+ }
+
+ const shim = this.shims.get(id);
+ if (!shim) {
+ return;
+ }
+
+ const shimsChanged = [this.getShimInfoForAboutCompat(shim)];
+ this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({ shimsChanged });
+ }
+
+ getAvailableShims() {
+ const shims = Array.from(this.shims.values()).map(
+ this.getShimInfoForAboutCompat
+ );
+ shims.sort((a, b) => a.name.localeCompare(b.name));
+ return shims;
+ }
+
+ _registerShims(shims) {
+ if (this.shims) {
+ throw new Error("_registerShims has already been called");
+ }
+
+ this.shims = new Map();
+ for (const shimOpts of shims) {
+ const { id } = shimOpts;
+ if (!this.shims.has(id)) {
+ this.shims.set(shimOpts.id, new Shim(shimOpts, this));
+ }
+ }
+
+ // Register onBeforeRequest listener which handles storage access requests
+ // on matching redirects.
+ let redirectTargetUrls = Array.from(shims.values())
+ .filter(shim => shim.requestStorageAccessForRedirect)
+ .flatMap(shim => shim.requestStorageAccessForRedirect)
+ .map(([, dstUrl]) => dstUrl);
+
+ // Unique target urls.
+ redirectTargetUrls = Array.from(new Set(redirectTargetUrls));
+
+ if (redirectTargetUrls.length) {
+ debug("Registering redirect listener for requestStorageAccess helper", {
+ redirectTargetUrls,
+ });
+ browser.webRequest.onBeforeRequest.addListener(
+ this._onRequestStorageAccessRedirect.bind(this),
+ { urls: redirectTargetUrls, types: ["main_frame"] },
+ ["blocking"]
+ );
+ }
+
+ function addTypePatterns(type, patterns, set) {
+ if (!set.has(type)) {
+ set.set(type, { patterns: new Set() });
+ }
+ const allSet = set.get(type).patterns;
+ for (const pattern of patterns) {
+ allSet.add(pattern);
+ }
+ }
+
+ const allMatchTypePatterns = new Map();
+ const allHeaderChangingMatchTypePatterns = new Map();
+ const allLogos = [];
+ for (const shim of this.shims.values()) {
+ const { logos, matches } = shim;
+ allLogos.push(...logos);
+ for (const { patterns, target, types } of matches || []) {
+ for (const type of types) {
+ if (shim.isGoogleTrendsDFPIFix) {
+ addTypePatterns(type, patterns, allHeaderChangingMatchTypePatterns);
+ }
+ if (target || shim.file || shim.runFirst) {
+ addTypePatterns(type, patterns, allMatchTypePatterns);
+ }
+ }
+ }
+ }
+
+ if (allLogos.length) {
+ const urls = Array.from(new Set(allLogos)).map(l => {
+ return `${LogosBaseURL}${l}`;
+ });
+ debug("Allowing access to these logos:", urls);
+ const unmarkShimsActive = tabId => {
+ for (const shim of this.shims.values()) {
+ shim.setActiveOnTab(tabId, false);
+ }
+ };
+ browser.tabs.onRemoved.addListener(unmarkShimsActive);
+ browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
+ if (changeInfo.discarded || changeInfo.url) {
+ unmarkShimsActive(tabId);
+ }
+ });
+ browser.webRequest.onBeforeRequest.addListener(
+ this._redirectLogos.bind(this),
+ { urls, types: ["image"] },
+ ["blocking"]
+ );
+ }
+
+ if (allHeaderChangingMatchTypePatterns) {
+ for (const [
+ type,
+ { patterns },
+ ] of allHeaderChangingMatchTypePatterns.entries()) {
+ const urls = Array.from(patterns);
+ debug("Shimming these", type, "URLs:", urls);
+ browser.webRequest.onBeforeSendHeaders.addListener(
+ this._onBeforeSendHeaders.bind(this),
+ { urls, types: [type] },
+ ["blocking", "requestHeaders"]
+ );
+ browser.webRequest.onHeadersReceived.addListener(
+ this._onHeadersReceived.bind(this),
+ { urls, types: [type] },
+ ["blocking", "responseHeaders"]
+ );
+ }
+ }
+
+ if (!allMatchTypePatterns.size) {
+ debug("Skipping shims; none enabled");
+ return;
+ }
+
+ for (const [type, { patterns }] of allMatchTypePatterns.entries()) {
+ const urls = Array.from(patterns);
+ debug("Shimming these", type, "URLs:", urls);
+
+ browser.webRequest.onBeforeRequest.addListener(
+ this._ensureShimForRequestOnTab.bind(this),
+ { urls, types: [type] },
+ ["blocking"]
+ );
+ }
+ }
+
+ async _checkEnabledPref() {
+ await browser.aboutConfigPrefs.getPref(this.ENABLED_PREF).then(value => {
+ if (value === undefined) {
+ browser.aboutConfigPrefs.setPref(this.ENABLED_PREF, true);
+ } else if (value === false) {
+ this.enabled = false;
+ } else {
+ this.enabled = true;
+ }
+ });
+ }
+
+ get enabled() {
+ return this._enabled;
+ }
+
+ set enabled(enabled) {
+ if (enabled === this._enabled) {
+ return;
+ }
+
+ this._enabled = enabled;
+
+ for (const shim of this.shims.values()) {
+ if (enabled) {
+ shim.onAllShimsEnabled();
+ } else {
+ shim.onAllShimsDisabled();
+ }
+ }
+ }
+
+ async _onRequestStorageAccessRedirect({
+ originUrl: srcUrl,
+ url: dstUrl,
+ tabId,
+ }) {
+ debug("Detected redirect", { srcUrl, dstUrl, tabId });
+
+ // Check if a shim needs to request storage access for this redirect. This
+ // handler is called when the *source url* matches a shims redirect pattern,
+ // but we still need to check if the *destination url* matches.
+ const matchingShims = Array.from(this.shims.values()).filter(shim => {
+ const { enabled, requestStorageAccessForRedirect } = shim;
+
+ if (!enabled || !requestStorageAccessForRedirect) {
+ return false;
+ }
+
+ return requestStorageAccessForRedirect.some(
+ ([srcPattern, dstPattern]) =>
+ browser.matchPatterns.getMatcher([srcPattern]).matches(srcUrl) &&
+ browser.matchPatterns.getMatcher([dstPattern]).matches(dstUrl)
+ );
+ });
+
+ // For each matching shim, find out if its enabled in regard to dFPI state.
+ const bugNumbers = new Set();
+ let isDFPIActive = null;
+ await Promise.all(
+ matchingShims.map(async shim => {
+ if (shim.onlyIfDFPIActive) {
+ // Only get the dFPI state for the first shim which requires it.
+ if (isDFPIActive === null) {
+ const tabIsPB = (await browser.tabs.get(tabId)).incognito;
+ isDFPIActive = await browser.trackingProtection.isDFPIActive(
+ tabIsPB
+ );
+ }
+ if (!isDFPIActive) {
+ return;
+ }
+ }
+ bugNumbers.add(shim.bug);
+ })
+ );
+
+ // If there is no shim which needs storage access for this redirect src/dst
+ // pair, resume it.
+ if (!bugNumbers.size) {
+ return;
+ }
+
+ // Inject the helper to call requestStorageAccessForOrigin on the document.
+ await browser.tabs.executeScript(tabId, {
+ file: "/lib/requestStorageAccess_helper.js",
+ runAt: "document_start",
+ });
+
+ const bugUrls = Array.from(bugNumbers)
+ .map(bugNo => `https://bugzilla.mozilla.org/show_bug.cgi?id=${bugNo}`)
+ .join(", ");
+ const warning = `Firefox calls the Storage Access API for ${dstUrl} on behalf of ${srcUrl}. See the following bugs for details: ${bugUrls}`;
+
+ // Request storage access for the origin of the destination url of the
+ // redirect.
+ const { origin: requestStorageAccessOrigin } = new URL(dstUrl);
+
+ // Wait for the requestStorageAccess request to finish before resuming the
+ // redirect.
+ const { success } = await browser.tabs.sendMessage(tabId, {
+ requestStorageAccessOrigin,
+ warning,
+ });
+ debug("requestStorageAccess callback", {
+ success,
+ requestStorageAccessOrigin,
+ srcUrl,
+ dstUrl,
+ bugNumbers,
+ });
+ }
+
+ async _onMessageFromShim(payload, sender, sendResponse) {
+ const { tab, frameId } = sender;
+ const { id, url } = tab;
+ const { shimId, message } = payload;
+
+ // Ignore unknown messages (for instance, from about:compat).
+ if (message !== "getOptions" && message !== "optIn") {
+ return undefined;
+ }
+
+ if (sender.id !== browser.runtime.id || id === -1) {
+ throw new Error("not allowed");
+ }
+
+ // Important! It is entirely possible for sites to spoof
+ // these messages, due to shims allowing web pages to
+ // communicate with the extension.
+
+ const shim = this.shims.get(shimId);
+ if (!shim?.needsShimHelpers?.includes(message)) {
+ throw new Error("not allowed");
+ }
+
+ if (message === "getOptions") {
+ return Object.assign(
+ {
+ platform: await platformPromise,
+ releaseBranch: await releaseBranchPromise,
+ },
+ shim.options
+ );
+ } else if (message === "optIn") {
+ try {
+ await shim.onUserOptIn(new URL(url).hostname);
+ const origin = new URL(tab.url).origin;
+ warn(
+ "** User opted in for",
+ shim.name,
+ "shim on",
+ origin,
+ "on tab",
+ id,
+ "frame",
+ frameId
+ );
+ await shim.showOptInWarningOnce(id, origin);
+ } catch (err) {
+ console.error(err);
+ throw new Error("error");
+ }
+ }
+
+ return undefined;
+ }
+
+ async _redirectLogos(details) {
+ await this._haveCheckedEnabledPref;
+
+ if (!this.enabled) {
+ return { cancel: true };
+ }
+
+ const { tabId, url } = details;
+ const logo = new URL(url).pathname.slice(1);
+
+ for (const shim of this.shims.values()) {
+ await shim.ready;
+
+ if (!shim.enabled) {
+ continue;
+ }
+
+ if (shim.onlyIfDFPIActive) {
+ const isPB = (await browser.tabs.get(details.tabId)).incognito;
+ if (!(await browser.trackingProtection.isDFPIActive(isPB))) {
+ continue;
+ }
+ }
+
+ if (!shim.logos.includes(logo)) {
+ continue;
+ }
+
+ if (shim.isActiveOnTab(tabId)) {
+ return { redirectUrl: browser.runtime.getURL(`shims/${logo}`) };
+ }
+ }
+
+ return { cancel: true };
+ }
+
+ async _onHeadersReceived(details) {
+ await this._haveCheckedEnabledPref;
+
+ for (const shim of this.shims.values()) {
+ await shim.ready;
+
+ if (!shim.enabled) {
+ continue;
+ }
+
+ if (shim.onlyIfDFPIActive) {
+ const isPB = (await browser.tabs.get(details.tabId)).incognito;
+ if (!(await browser.trackingProtection.isDFPIActive(isPB))) {
+ continue;
+ }
+ }
+
+ if (shim.isGoogleTrendsDFPIFix) {
+ if (shim.GoogleNidCookieToUse) {
+ continue;
+ }
+
+ for (const header of details.responseHeaders) {
+ if (header.name == "set-cookie") {
+ shim.GoogleNidCookieToUse = header.value;
+ return { redirectUrl: details.url };
+ }
+ }
+ }
+ }
+
+ return undefined;
+ }
+
+ async _onBeforeSendHeaders(details) {
+ await this._haveCheckedEnabledPref;
+
+ const { frameId, requestHeaders, tabId } = details;
+
+ if (!this.enabled) {
+ return { requestHeaders };
+ }
+
+ for (const shim of this.shims.values()) {
+ await shim.ready;
+
+ if (!shim.enabled) {
+ continue;
+ }
+
+ if (shim.isGoogleTrendsDFPIFix) {
+ const value = shim.GoogleNidCookieToUse;
+
+ if (!value) {
+ continue;
+ }
+
+ let found;
+ for (let header of requestHeaders) {
+ if (header.name.toLowerCase() === "cookie") {
+ header.value = value;
+ found = true;
+ }
+ }
+ if (!found) {
+ requestHeaders.push({ name: "Cookie", value });
+ }
+
+ browser.tabs
+ .get(tabId)
+ .then(({ url }) => {
+ debug(
+ `Google Trends dFPI fix used on tab ${tabId} frame ${frameId} (${url})`
+ );
+ })
+ .catch(() => {});
+
+ const warning = `Working around Google Trends tracking protection breakage. See https://bugzilla.mozilla.org/show_bug.cgi?id=${shim.bug} for details.`;
+ browser.tabs
+ .executeScript(tabId, {
+ code: `console.warn(${JSON.stringify(warning)})`,
+ runAt: "document_start",
+ })
+ .catch(() => {});
+ }
+ }
+
+ return { requestHeaders };
+ }
+
+ async _ensureShimForRequestOnTab(details) {
+ await this._haveCheckedEnabledPref;
+
+ if (!this.enabled) {
+ return undefined;
+ }
+
+ // We only ever reach this point if a request is for a URL which ought to
+ // be shimmed. We never get here if a request is blocked, and we only
+ // unblock requests if at least one shim matches it.
+
+ const { frameId, originUrl, requestId, tabId, type, url } = details;
+
+ // Ignore requests unrelated to tabs
+ if (tabId < 0) {
+ return undefined;
+ }
+
+ // We need to base our checks not on the frame's host, but the tab's.
+ const topHost = new URL((await browser.tabs.get(tabId)).url).hostname;
+ const unblocked = await browser.trackingProtection.wasRequestUnblocked(
+ requestId
+ );
+
+ let match;
+ let shimToApply;
+ for (const shim of this.shims.values()) {
+ await shim.ready;
+
+ if (!shim.enabled || (!shim.redirectsRequests && !shim.runFirst)) {
+ continue;
+ }
+
+ if (shim.onlyIfDFPIActive || shim.onlyIfPrivateBrowsing) {
+ const isPB = (await browser.tabs.get(details.tabId)).incognito;
+ if (!isPB && shim.onlyIfPrivateBrowsing) {
+ continue;
+ }
+ if (
+ shim.onlyIfDFPIActive &&
+ !(await browser.trackingProtection.isDFPIActive(isPB))
+ ) {
+ continue;
+ }
+ }
+
+ // Do not apply the shim if it is only meant to apply when strict mode ETP
+ // (content blocking) was going to block the request.
+ if (!unblocked && shim.onlyIfBlockedByETP) {
+ continue;
+ }
+
+ if (!shim.meantForHost(topHost)) {
+ continue;
+ }
+
+ // If this URL and content type isn't meant for this shim, don't apply it.
+ match = shim.isTriggeredByURLAndType(url, type);
+ if (match) {
+ if (!unblocked && match.onlyIfBlockedByETP) {
+ continue;
+ }
+
+ // If the user has already opted in for this shim, all requests it covers
+ // should be allowed; no need for a shim anymore.
+ if (shim.hasUserOptedInAlready(topHost)) {
+ warn(
+ `Allowing tracking ${type} ${url} on tab ${tabId} frame ${frameId} due to opt-in`
+ );
+ shim.showOptInWarningOnce(tabId, new URL(originUrl).origin);
+ return undefined;
+ }
+ shimToApply = shim;
+ break;
+ }
+ }
+
+ let runFirst = false;
+
+ if (shimToApply) {
+ // Note that sites may request the same shim twice, but because the requests
+ // may differ enough for some to fail (CSP/CORS/etc), we always let the request
+ // complete via local redirect. Shims should gracefully handle this as well.
+
+ const { target } = match;
+ const { bug, file, id, name, needsShimHelpers } = shimToApply;
+ runFirst = shimToApply.runFirst;
+
+ const redirect = target || file;
+
+ warn(
+ `Shimming tracking ${type} ${url} on tab ${tabId} frame ${frameId} with ${
+ redirect || runFirst
+ }`
+ );
+
+ const warning = `${name} is being shimmed by Firefox. See https://bugzilla.mozilla.org/show_bug.cgi?id=${bug} for details.`;
+
+ let needConsoleMessage = true;
+
+ if (runFirst) {
+ try {
+ await browser.tabs.executeScript(tabId, {
+ file: `/shims/${runFirst}`,
+ frameId,
+ runAt: "document_start",
+ });
+ } catch (_) {}
+ }
+
+ // For scripts, we also set up any needed shim helpers.
+ if (type === "script" && needsShimHelpers?.length) {
+ try {
+ await browser.tabs.executeScript(tabId, {
+ file: "/lib/shim_messaging_helper.js",
+ frameId,
+ runAt: "document_start",
+ });
+ const origin = new URL(originUrl).origin;
+ await browser.tabs.sendMessage(
+ tabId,
+ { origin, shimId: id, needsShimHelpers, warning },
+ { frameId }
+ );
+ needConsoleMessage = false;
+ shimToApply.setActiveOnTab(tabId);
+ } catch (_) {}
+ }
+
+ if (needConsoleMessage) {
+ try {
+ await browser.tabs.executeScript(tabId, {
+ code: `console.warn(${JSON.stringify(warning)})`,
+ runAt: "document_start",
+ });
+ } catch (_) {}
+ }
+
+ if (!redirect.indexOf("http://") || !redirect.indexOf("https://")) {
+ return { redirectUrl: redirect };
+ }
+
+ // If any shims matched the request to replace it, then redirect to the local
+ // file bundled with SmartBlock, so the request never hits the network.
+ return { redirectUrl: browser.runtime.getURL(`shims/${redirect}`) };
+ }
+
+ // Sanity check: if no shims end up handling this request,
+ // yet it was meant to be blocked by ETP, then block it now.
+ if (unblocked) {
+ error(`unexpected: ${url} not shimmed on tab ${tabId} frame ${frameId}`);
+ return { cancel: true };
+ }
+
+ if (!runFirst) {
+ debug(`ignoring ${url} on tab ${tabId} frame ${frameId}`);
+ }
+ return undefined;
+ }
+}
+
+module.exports = Shims;