diff options
Diffstat (limited to 'browser/extensions/webcompat')
96 files changed, 6644 insertions, 0 deletions
diff --git a/browser/extensions/webcompat/about-compat/AboutCompat.jsm b/browser/extensions/webcompat/about-compat/AboutCompat.jsm new file mode 100644 index 0000000000..a90bd8ed80 --- /dev/null +++ b/browser/extensions/webcompat/about-compat/AboutCompat.jsm @@ -0,0 +1,36 @@ +/* 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"; + +var EXPORTED_SYMBOLS = ["AboutCompat"]; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const addonID = "webcompat@mozilla.org"; +const addonPageRelativeURL = "/about-compat/aboutCompat.html"; + +function AboutCompat() { + this.chromeURL = WebExtensionPolicy.getByID(addonID).getURL( + addonPageRelativeURL + ); +} +AboutCompat.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIAboutModule"]), + getURIFlags() { + return Ci.nsIAboutModule.URI_MUST_LOAD_IN_EXTENSION_PROCESS; + }, + + newChannel(aURI, aLoadInfo) { + const uri = Services.io.newURI(this.chromeURL); + const channel = Services.io.newChannelFromURIWithLoadInfo(uri, aLoadInfo); + channel.originalURI = aURI; + + channel.owner = ( + Services.scriptSecurityManager.createContentPrincipal || + Services.scriptSecurityManager.createCodebasePrincipal + )(uri, aLoadInfo.originAttributes); + return channel; + }, +}; diff --git a/browser/extensions/webcompat/about-compat/aboutCompat.css b/browser/extensions/webcompat/about-compat/aboutCompat.css new file mode 100644 index 0000000000..492296dfcf --- /dev/null +++ b/browser/extensions/webcompat/about-compat/aboutCompat.css @@ -0,0 +1,187 @@ +/* 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/. */ + +@media (any-pointer: fine) { + :root { + font-family: sans-serif; + margin: 40px auto; + min-width: 30em; + max-width: 60em; + } + + table { + width: 100%; + padding-bottom: 2em; + } + + .hidden { + display: none; + } + + .table-title-container { + align-items: center; + display: flex; + justify-content: space-between; + } + + .wide-button { + display: block; + min-height: 32px; + padding-inline: 30px; + } + + .submitting { + background-image: url(chrome://global/skin/icons/loading.png); + background-position: center; + background-repeat: no-repeat; + background-size: 16px; + } + + .submitting .submit-crash-button-label { + display: none; + } + + .failed-to-submit { + color: #ca8695; + } + + a.button-as-link { + appearance: none; + min-height: 30px; + color: var(--in-content-text-color) !important; + border: 1px solid var(--in-content-box-border-color) !important; + border-radius: 2px; + background-color: var(--in-content-page-background); + line-height: 30px; + margin: 4px 8px; + /* Ensure font-size isn't overridden by widget styling (e.g. in forms.css) */ + font-size: 1em; + } + + a.button-as-link:hover { + background-color: var(--in-content-box-background-hover) !important; + text-decoration: none; + } + + h2.lighter-font-weight { + font-weight: lighter; + } + + th { + text-align: start; + } +} + +@media (any-pointer: coarse), (any-pointer: none) { + * { + margin: 0; + padding: 0; + } + + html { + font-family: sans-serif; + font-size: 14px; + -moz-text-size-adjust: none; + background-color: #f5f5f5; + } + + table, + tr, + p { + display: block; + background: #fff; + } + + table { + border-top: 2px solid #0a84ff; + margin-top: -2px; + position: absolute; + width: 100%; + z-index: 1; + display: none; + } + + tr { + position: relative; + border-bottom: 1px solid #d7d9db; + padding: 1em; + } + + a { + color: #000; + font-size: 94%; + } + + .tab { + cursor: pointer; + position: relative; + z-index: 2; + display: inline-block; + text-align: left; + padding: 1em; + font-weight: bold; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + border: 1px solid #d7d9db; + border-bottom: 0; + margin-bottom: 2px; + background: #f5f5f5; + color: #363b40; + font-size: 1em; + font-weight: bold; + padding: 1em; + } + + .tab.active { + border-bottom-color: #fff; + background: #fff; + margin-bottom: 0; + padding-bottom: calc(1em + 2px); + } + + .tab.active + table { + display: block; + } + + td { + display: block; + position: relative; + padding-inline-end: 6.5em; + } + + td[colspan="4"] { + padding: 1em; + font-style: italic; + text-align: center; + } + + td:not([colspan]):nth-child(1) { + font-weight: bold; + } + + td:not([colspan]):nth-child(1) { + padding-bottom: 0.25em; + } + + td:nth-child(3) { + display: contents; + } + + button { + background: #e8e8e7; + position: absolute; + top: 0; + bottom: 0; + inset-inline-end: 0; + width: 6em; + border: 0; + border-inline-start: 1px solid #d7d9db; + appearance: none; + color: #000; + } + + button::-moz-focus-inner { + border: 0; + } +} diff --git a/browser/extensions/webcompat/about-compat/aboutCompat.html b/browser/extensions/webcompat/about-compat/aboutCompat.html new file mode 100644 index 0000000000..31180f0b30 --- /dev/null +++ b/browser/extensions/webcompat/about-compat/aboutCompat.html @@ -0,0 +1,37 @@ +<!-- 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/. --> + +<!DOCTYPE HTML> +<html> +<head> + <base/> + + <!-- If you change this script tag you must update the hash in the extension's + `content_security_policy` 'sha256-MmZkN2QaIHhfRWPZ8TVRjijTn5Ci1iEabtTEWrt9CCo=' --> + <script>/* globals browser */ document.head.firstElementChild.href = browser.runtime.getURL("");</script> + + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link rel="stylesheet" href="about-compat/aboutCompat.css" /> + <link rel="stylesheet" media="screen and (pointer:fine), projection" type="text/css" + href="chrome://global/skin/in-content/common.css"/> + <link rel="localization" href="toolkit/about/aboutCompat.ftl"/> + <title data-l10n-id="text-title"></title> + <script src="about-compat/aboutCompat.js"></script> + </head> +<body> + <h2 class="tab active" data-l10n-id="label-overrides"></h2> + <table id="overrides"> + <col/> + <col/> + <col/> + </table> + <h2 class="tab" data-l10n-id="label-interventions"></h2> + <table id="interventions"> + <col/> + <col/> + <col/> + </table> +</body> +</html> diff --git a/browser/extensions/webcompat/about-compat/aboutCompat.js b/browser/extensions/webcompat/about-compat/aboutCompat.js new file mode 100644 index 0000000000..e5c9c4ff17 --- /dev/null +++ b/browser/extensions/webcompat/about-compat/aboutCompat.js @@ -0,0 +1,171 @@ +/* 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 */ + +let availablePatches; + +const portToAddon = (function() { + let port; + + function connect() { + port = browser.runtime.connect({ name: "AboutCompatTab" }); + port.onMessage.addListener(onMessageFromAddon); + port.onDisconnect.addListener(e => { + port = undefined; + }); + } + + connect(); + + async function send(message) { + if (port) { + return port.postMessage(message); + } + return Promise.reject("background script port disconnected"); + } + + return { send }; +})(); + +const $ = function(sel) { + return document.querySelector(sel); +}; + +const DOMContentLoadedPromise = new Promise(resolve => { + document.addEventListener( + "DOMContentLoaded", + () => { + resolve(); + }, + { once: true } + ); +}); + +Promise.all([ + browser.runtime.sendMessage("getOverridesAndInterventions"), + DOMContentLoadedPromise, +]).then(([info]) => { + document.body.addEventListener("click", async evt => { + const ele = evt.target; + if (ele.nodeName === "BUTTON") { + const row = ele.closest("[data-id]"); + if (row) { + evt.preventDefault(); + ele.disabled = true; + const id = row.getAttribute("data-id"); + try { + await browser.runtime.sendMessage({ command: "toggle", id }); + } catch (_) { + ele.disabled = false; + } + } + } else if (ele.classList.contains("tab")) { + document.querySelectorAll(".tab").forEach(tab => { + tab.classList.remove("active"); + }); + ele.classList.add("active"); + } + }); + + availablePatches = info; + redraw(); +}); + +function onMessageFromAddon(msg) { + if ("interventionsChanged" in msg) { + redrawTable($("#interventions"), msg.interventionsChanged); + } + + if ("overridesChanged" in msg) { + redrawTable($("#overrides"), msg.overridesChanged); + } + + const id = msg.toggling || msg.toggled; + const button = $(`[data-id="${id}"] button`); + if (!button) { + return; + } + const active = msg.active; + document.l10n.setAttributes( + button, + active ? "label-disable" : "label-enable" + ); + button.disabled = !!msg.toggling; +} + +function redraw() { + if (!availablePatches) { + return; + } + const { overrides, interventions } = availablePatches; + const showHidden = location.hash === "#all"; + redrawTable($("#overrides"), overrides, showHidden); + redrawTable($("#interventions"), interventions, showHidden); +} + +function redrawTable(table, data, showHidden = false) { + const df = document.createDocumentFragment(); + table.querySelectorAll("tr").forEach(tr => { + tr.remove(); + }); + + let noEntriesMessage; + if (data === false) { + noEntriesMessage = "text-disabled-in-about-config"; + } else if (data.length === 0) { + noEntriesMessage = + table.id === "overrides" ? "text-no-overrides" : "text-no-interventions"; + } + + if (noEntriesMessage) { + const tr = document.createElement("tr"); + df.appendChild(tr); + + const td = document.createElement("td"); + td.setAttribute("colspan", "3"); + document.l10n.setAttributes(td, noEntriesMessage); + tr.appendChild(td); + + table.appendChild(df); + return; + } + + for (const row of data) { + if (row.hidden && !showHidden) { + continue; + } + + const tr = document.createElement("tr"); + tr.setAttribute("data-id", row.id); + df.appendChild(tr); + + let td = document.createElement("td"); + td.innerText = row.domain; + tr.appendChild(td); + + td = document.createElement("td"); + const a = document.createElement("a"); + const bug = row.bug; + a.href = `https://bugzilla.mozilla.org/show_bug.cgi?id=${bug}`; + document.l10n.setAttributes(a, "label-more-information", { bug }); + a.target = "_blank"; + td.appendChild(a); + tr.appendChild(td); + + td = document.createElement("td"); + tr.appendChild(td); + const button = document.createElement("button"); + document.l10n.setAttributes( + button, + row.active ? "label-disable" : "label-enable" + ); + td.appendChild(button); + } + table.appendChild(df); +} + +window.onhashchange = redraw; diff --git a/browser/extensions/webcompat/about-compat/aboutPage.js b/browser/extensions/webcompat/about-compat/aboutPage.js new file mode 100644 index 0000000000..e719551332 --- /dev/null +++ b/browser/extensions/webcompat/about-compat/aboutPage.js @@ -0,0 +1,48 @@ +/* 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"; + +/* global ExtensionAPI, Services, XPCOMUtils */ + +ChromeUtils.defineModuleGetter( + this, + "Services", + "resource://gre/modules/Services.jsm" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "resProto", + "@mozilla.org/network/protocol;1?name=resource", + "nsISubstitutingProtocolHandler" +); + +const ResourceSubstitution = "webcompat"; +const ProcessScriptURL = "resource://webcompat/aboutPageProcessScript.js"; +const ContractID = "@mozilla.org/network/protocol/about;1?what=compat"; + +this.aboutPage = class extends ExtensionAPI { + onStartup() { + const { rootURI } = this.extension; + + resProto.setSubstitution( + ResourceSubstitution, + Services.io.newURI("about-compat/", null, rootURI) + ); + + if (!(ContractID in Cc)) { + Services.ppmm.loadProcessScript(ProcessScriptURL, true); + this.processScriptRegistered = true; + } + } + + onShutdown() { + resProto.setSubstitution(ResourceSubstitution, null); + + if (this.processScriptRegistered) { + Services.ppmm.removeDelayedProcessScript(ProcessScriptURL); + } + } +}; diff --git a/browser/extensions/webcompat/about-compat/aboutPage.json b/browser/extensions/webcompat/about-compat/aboutPage.json new file mode 100644 index 0000000000..42e6114188 --- /dev/null +++ b/browser/extensions/webcompat/about-compat/aboutPage.json @@ -0,0 +1,6 @@ +[ + { + "namespace": "aboutCompat", + "description": "Enables the about:compat page" + } +] diff --git a/browser/extensions/webcompat/about-compat/aboutPageProcessScript.js b/browser/extensions/webcompat/about-compat/aboutPageProcessScript.js new file mode 100644 index 0000000000..e2e9866eb1 --- /dev/null +++ b/browser/extensions/webcompat/about-compat/aboutPageProcessScript.js @@ -0,0 +1,32 @@ +/* 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"; + +// Note: This script is used only when a static registration for our +// component is not already present in the libxul binary. + +const Cm = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + +const classID = Components.ID("{97bf9550-2a7b-11e9-b56e-0800200c9a66}"); + +if (!Cm.isCIDRegistered(classID)) { + const { ComponentUtils } = ChromeUtils.import( + "resource://gre/modules/ComponentUtils.jsm" + ); + + const factory = ComponentUtils.generateSingletonFactory(function() { + const { AboutCompat } = ChromeUtils.import( + "resource://webcompat/AboutCompat.jsm" + ); + return new AboutCompat(); + }); + + Cm.registerFactory( + classID, + "about:compat", + "@mozilla.org/network/protocol/about;1?what=compat", + factory + ); +} diff --git a/browser/extensions/webcompat/components.conf b/browser/extensions/webcompat/components.conf new file mode 100644 index 0000000000..ca5a6c3dbd --- /dev/null +++ b/browser/extensions/webcompat/components.conf @@ -0,0 +1,17 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +# Note: This file will add static component registration entries for our +# components to the libxul binary, though the actual component JSMs will be +# packaged with the extension. +Classes = [ + { + 'cid': '{97bf9550-2a7b-11e9-b56e-0800200c9a66}', + 'contract_ids': ['@mozilla.org/network/protocol/about;1?what=compat'], + 'jsm': 'resource://webcompat/AboutCompat.jsm', + 'constructor': 'AboutCompat', + }, +] diff --git a/browser/extensions/webcompat/data/injections.js b/browser/extensions/webcompat/data/injections.js new file mode 100644 index 0000000000..d97deca4d6 --- /dev/null +++ b/browser/extensions/webcompat/data/injections.js @@ -0,0 +1,467 @@ +/* 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 module, require */ + +// This is a hack for the tests. +if (typeof InterventionHelpers === "undefined") { + var InterventionHelpers = require("../lib/intervention_helpers"); +} + +/** + * For detailed information on our policies, and a documention on this format + * and its possibilites, please check the Mozilla-Wiki at + * + * https://wiki.mozilla.org/Compatibility/Go_Faster_Addon/Override_Policies_and_Workflows#User_Agent_overrides + */ +const AVAILABLE_INJECTIONS = [ + { + id: "testbed-injection", + platform: "all", + domain: "webcompat-addon-testbed.herokuapp.com", + bug: "0000000", + hidden: true, + contentScripts: { + matches: ["*://webcompat-addon-testbed.herokuapp.com/*"], + css: [ + { + file: "injections/css/bug0000000-testbed-css-injection.css", + }, + ], + js: [ + { + file: "injections/js/bug0000000-testbed-js-injection.js", + }, + ], + }, + }, + { + id: "bug1452707", + platform: "desktop", + domain: "ib.absa.co.za", + bug: "1452707", + contentScripts: { + matches: ["https://ib.absa.co.za/*"], + js: [ + { + file: + "injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js", + }, + ], + }, + }, + { + id: "bug1457335", + platform: "desktop", + domain: "histography.io", + bug: "1457335", + contentScripts: { + matches: ["*://histography.io/*"], + js: [ + { + file: "injections/js/bug1457335-histography.io-ua-change.js", + }, + ], + }, + }, + { + id: "bug1472075", + platform: "desktop", + domain: "bankofamerica.com", + bug: "1472075", + contentScripts: { + matches: ["*://*.bankofamerica.com/*"], + js: [ + { + file: "injections/js/bug1472075-bankofamerica.com-ua-change.js", + }, + ], + }, + }, + { + id: "bug1570856", + platform: "android", + domain: "medium.com", + bug: "1570856", + contentScripts: { + matches: ["*://medium.com/*"], + js: [ + { + file: "injections/js/bug1570856-medium.com-menu-isTier1.js", + }, + ], + allFrames: true, + }, + }, + { + id: "bug1579159", + platform: "android", + domain: "m.tailieu.vn", + bug: "1579159", + contentScripts: { + matches: ["*://m.tailieu.vn/*", "*://m.elib.vn/*"], + js: [ + { + file: "injections/js/bug1579159-m.tailieu.vn-pdfjs-worker-disable.js", + }, + ], + allFrames: true, + }, + }, + { + id: "bug1551672", + platform: "android", + domain: "Sites using PDK 5 video", + bug: "1551672", + data: { + urls: ["https://*/*/tpPdk.js", "https://*/*/pdk/js/*/*.js"], + types: ["script"], + }, + customFunc: "pdk5fix", + }, + { + id: "bug1583366", + platform: "desktop", + domain: "Download prompt for files with no content-type", + bug: "1583366", + data: { + urls: ["https://ads-us.rd.linksynergy.com/as.php*"], + contentType: { + name: "content-type", + value: "text/html; charset=utf-8", + }, + }, + customFunc: "noSniffFix", + }, + { + id: "bug1561371", + platform: "android", + domain: "mail.google.com", + bug: "1561371", + contentScripts: { + matches: ["*://mail.google.com/*"], + css: [ + { + file: + "injections/css/bug1561371-mail.google.com-allow-horizontal-scrolling.css", + }, + ], + }, + }, + { + id: "bug1570119", + platform: "desktop", + domain: "teamcoco.com", + bug: "1570119", + contentScripts: { + matches: ["*://teamcoco.com/*"], + css: [ + { + file: "injections/css/bug1570119-teamcoco.com-scrollbar-width.css", + }, + ], + }, + }, + { + id: "bug1570328", + platform: "android", + domain: "developer.apple.com", + bug: "1570328", + contentScripts: { + matches: ["*://developer.apple.com/*"], + css: [ + { + file: + "injections/css/bug1570328-developer-apple.com-transform-scale.css", + }, + ], + }, + }, + { + id: "bug1575000", + platform: "all", + domain: "apply.lloydsbank.co.uk", + bug: "1575000", + contentScripts: { + matches: ["*://apply.lloydsbank.co.uk/*"], + css: [ + { + file: + "injections/css/bug1575000-apply.lloydsbank.co.uk-radio-buttons-fix.css", + }, + ], + }, + }, + { + id: "bug1605611", + platform: "android", + domain: "maps.google.com", + bug: "1605611", + contentScripts: { + matches: InterventionHelpers.matchPatternsForGoogle( + "*://www.google.", + "/maps*" + ), + css: [ + { + file: "injections/css/bug1605611-maps.google.com-directions-time.css", + }, + ], + js: [ + { + file: "injections/js/bug1605611-maps.google.com-directions-time.js", + }, + ], + }, + }, + { + id: "bug1610016", + platform: "android", + domain: "gaana.com", + bug: "1610016", + contentScripts: { + matches: ["https://gaana.com/*"], + css: [ + { + file: "injections/css/bug1610016-gaana.com-input-position-fix.css", + }, + ], + }, + }, + { + id: "bug1610358", + platform: "android", + domain: "pcloud.com", + bug: "1610358", + contentScripts: { + matches: ["https://www.pcloud.com/*"], + js: [ + { + file: "injections/js/bug1610358-pcloud.com-appVersion-change.js", + }, + ], + }, + }, + { + id: "bug1610344", + platform: "all", + domain: "directv.com.co", + bug: "1610344", + contentScripts: { + matches: ["https://*.directv.com.co/*"], + css: [ + { + file: + "injections/css/bug1610344-directv.com.co-hide-unsupported-message.css", + }, + ], + }, + }, + { + id: "bug1622062", + platform: "android", + domain: "$.detectSwipe fix", + bug: "1622062", + data: { + urls: ["https://eu.stemwijzer.nl/public/js/votematch.vendors.js"], + types: ["script"], + }, + customFunc: "detectSwipeFix", + }, + { + id: "bug1644830", + platform: "desktop", + domain: "usps.com", + bug: "1644830", + contentScripts: { + matches: ["https://*.usps.com/*"], + css: [ + { + file: + "injections/css/bug1644830-missingmail.usps.com-checkboxes-not-visible.css", + }, + ], + }, + }, + { + id: "bug1645064", + platform: "desktop", + domain: "s-kanava.fi", + bug: "1645064", + contentScripts: { + matches: ["https://www.s-kanava.fi/*"], + css: [ + { + file: "injections/css/bug1645064-s-kanava.fi-invisible-charts.css", + }, + ], + }, + }, + { + id: "bug1651917", + platform: "android", + domain: "teletrader.com", + bug: "1651917", + contentScripts: { + matches: ["*://*.teletrader.com/*"], + css: [ + { + file: + "injections/css/bug1651917-teletrader.com.body-transform-origin.css", + }, + ], + }, + }, + { + id: "bug1653075", + platform: "desktop", + domain: "livescience.com", + bug: "1653075", + contentScripts: { + matches: ["*://*.livescience.com/*"], + css: [ + { + file: "injections/css/bug1653075-livescience.com-scrollbar-width.css", + }, + ], + }, + }, + { + id: "bug1654865", + platform: "android", + domain: "sports.ndtv.com", + bug: "1654865", + contentScripts: { + matches: ["*://sports.ndtv.com/*"], + css: [ + { + file: "injections/css/bug1654865-sports.ndtv.com-float-fix.css", + }, + ], + }, + }, + { + id: "bug1654877", + platform: "android", + domain: "preev.com", + bug: "1654877", + contentScripts: { + matches: ["*://preev.com/*"], + css: [ + { + file: "injections/css/bug1654877-preev.com-moz-appearance-fix.css", + }, + ], + }, + }, + { + id: "bug1655049", + platform: "android", + domain: "dev.to", + bug: "1655049", + contentScripts: { + matches: ["*://dev.to/*"], + css: [ + { + file: "injections/css/bug1655049-dev.to-unclickable-button-fix.css", + }, + ], + }, + }, + { + id: "bug1654907", + platform: "android", + domain: "reactine.ca", + bug: "1654907", + contentScripts: { + matches: ["*://*.reactine.ca/*"], + css: [ + { + file: "injections/css/bug1654907-reactine.ca-hide-unsupported.css", + }, + ], + }, + }, + { + id: "bug1666771", + platform: "desktop", + domain: "zillow.com", + bug: "1666771", + contentScripts: { + allFrames: true, + matches: ["*://*.zillow.com/*"], + css: [ + { + file: "injections/css/bug1666771-zilow-map-overdraw.css", + }, + ], + }, + }, + { + id: "bug1631811", + platform: "all", + domain: "datastudio.google.com", + bug: "1631811", + contentScripts: { + matches: ["https://datastudio.google.com/embed/reporting/*"], + js: [ + { + file: "injections/js/bug1631811-datastudio.google.com-indexedDB.js", + }, + ], + allFrames: true, + }, + }, + { + id: "bug1665035", + platform: "desktop", + domain: "dckids.com", + bug: "1665035", + contentScripts: { + matches: [ + "https://d3qlaywcwingl6.cloudfront.net/content/*/Html5Game/*", + "https://d3qlaywcwingl6.cloudfront.net/*/game/content/*", + ], + js: [ + { + file: "injections/js/bug1665035-dckids.com-cookieEnabled.js", + }, + ], + allFrames: true, + }, + }, + { + id: "bug1677442", + platform: "desktop", + domain: "store.hp.com", + bug: "1677442", + contentScripts: { + matches: ["*://d3nkfb7815bs43.cloudfront.net/*forstore.hp.com*"], + js: [ + { + file: "injections/js/bug1677442-store.hp.com-disable-indexeddb.js", + }, + ], + allFrames: true, + }, + }, + { + id: "bug1682238", + platform: "desktop", + domain: "gamearter.com", + bug: "1682238", + contentScripts: { + matches: ["*://*.gamearter.com/*"], + js: [ + { + file: "injections/js/bug1682238-gamearter.com-ua-change.js", + }, + ], + }, + }, +]; + +module.exports = AVAILABLE_INJECTIONS; diff --git a/browser/extensions/webcompat/data/picture_in_picture_overrides.js b/browser/extensions/webcompat/data/picture_in_picture_overrides.js new file mode 100644 index 0000000000..e9a4e88657 --- /dev/null +++ b/browser/extensions/webcompat/data/picture_in_picture_overrides.js @@ -0,0 +1,60 @@ +/* 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 */ + +let AVAILABLE_PIP_OVERRIDES; + +{ + // See PictureInPictureControls.jsm for these values. + // eslint-disable-next-line no-unused-vars + const TOGGLE_POLICIES = browser.pictureInPictureChild.getPolicies(); + const KEYBOARD_CONTROLS = browser.pictureInPictureChild.getKeyboardControls(); + + AVAILABLE_PIP_OVERRIDES = { + // The keys of this object are match patterns for URLs, as documented in + // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns + // + // Example: + // + // "https://*.youtube.com/*": { + // policy: TOGGLE_POLICIES.THREE_QUARTERS, + // keyboardControls: KEYBOARD_CONTROLS.PLAY_PAUSE | KEYBOARD_CONTROLS.VOLUME, + // }, + // "https://*.twitch.tv/mikeconley_dot_ca/*": { + // policy: TOGGLE_POLICIES.TOP, + // keyboardControls: KEYBOARD_CONTROLS.NONE, + // }, + + instagram: { + "https://www.instagram.com/*": { policy: TOGGLE_POLICIES.ONE_QUARTER }, + }, + + laracasts: { + "https://*.laracasts.com/*": { policy: TOGGLE_POLICIES.ONE_QUARTER }, + }, + + netflix: { + "https://*.netflix.com/*": { keyboardControls: ~KEYBOARD_CONTROLS.SEEK }, + "https://*.netflix.com/browse": { policy: TOGGLE_POLICIES.HIDDEN }, + "https://*.netflix.com/latest": { policy: TOGGLE_POLICIES.HIDDEN }, + }, + + twitch: { + "https://*.twitch.tv/*": { policy: TOGGLE_POLICIES.ONE_QUARTER }, + "https://*.twitch.tech/*": { policy: TOGGLE_POLICIES.ONE_QUARTER }, + "https://*.twitch.a2z.com/*": { policy: TOGGLE_POLICIES.ONE_QUARTER }, + }, + + udemy: { + "https://*.udemy.com/*": { policy: TOGGLE_POLICIES.ONE_QUARTER }, + }, + + youtube: { + "https://*.youtube.com/*": { visibilityThreshold: 0.9 }, + }, + }; +} diff --git a/browser/extensions/webcompat/data/shims.js b/browser/extensions/webcompat/data/shims.js new file mode 100644 index 0000000000..8abdffac39 --- /dev/null +++ b/browser/extensions/webcompat/data/shims.js @@ -0,0 +1,253 @@ +/* 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 module, require */ + +const AVAILABLE_SHIMS = [ + { + id: "LiveTestShim", + platform: "all", + name: "Live test shim", + bug: "livetest", + file: "live-test-shim.js", + matches: ["*://webcompat-addon-testbed.herokuapp.com/shims_test.js"], + needsShimHelpers: ["getOptions", "optIn"], + }, + { + id: "MochitestShim", + platform: "all", + name: "Test shim for Mochitests", + bug: "mochitest", + file: "mochitest-shim-1.js", + matches: [ + "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test.js", + ], + needsShimHelpers: ["getOptions", "optIn"], + options: { + simpleOption: true, + complexOption: { a: 1, b: "test" }, + branchValue: { value: true, branches: [] }, + platformValue: { value: true, platform: "neverUsed" }, + }, + unblocksOnOptIn: ["*://trackertest.org/*"], + }, + { + disabled: true, + id: "MochitestShim2", + platform: "all", + name: "Test shim for Mochitests (disabled by default)", + bug: "mochitest", + file: "mochitest-shim-2.js", + matches: [ + "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_2.js", + ], + needsShimHelpers: ["getOptions", "optIn"], + options: { + simpleOption: true, + complexOption: { a: 1, b: "test" }, + branchValue: { value: true, branches: [] }, + platformValue: { value: true, platform: "neverUsed" }, + }, + unblocksOnOptIn: ["*://trackertest.org/*"], + }, + { + id: "MochitestShim3", + platform: "all", + name: "Test shim for Mochitests (host)", + bug: "mochitest", + file: "mochitest-shim-3.js", + notHosts: ["example.com"], + matches: [ + "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_3.js", + ], + }, + { + id: "MochitestShim4", + platform: "all", + name: "Test shim for Mochitests (notHost)", + bug: "mochitest", + file: "mochitest-shim-3.js", + hosts: ["example.net"], + matches: [ + "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_3.js", + ], + }, + { + id: "MochitestShim5", + platform: "all", + name: "Test shim for Mochitests (branch)", + bug: "mochitest", + file: "mochitest-shim-3.js", + branches: ["never matches"], + matches: [ + "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_3.js", + ], + }, + { + id: "MochitestShim6", + platform: "never matches", + name: "Test shim for Mochitests (platform)", + bug: "mochitest", + file: "mochitest-shim-3.js", + matches: [ + "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_3.js", + ], + }, + { + id: "AdSafeProtectedGoogleIMAAdapter", + platform: "all", + branches: ["nightly"], + name: "Ad Safe Protected Google IMA Adapter", + bug: "1508639", + file: "adsafeprotected-ima.js", + matches: ["*://static.adsafeprotected.com/vans-adapter-google-ima.js"], + needsShimHelpers: ["optIn"], + onlyIfBlockedByETP: true, + unblocksOnOptIn: ["*://pubads.g.doubleclick.net/gampad/ads"], + }, + { + id: "AdsByGoogle", + platform: "all", + branches: ["nightly"], + name: "Ads by Google", + bug: "1629644", + file: "empty-script.js", + matches: ["*://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"], + onlyIfBlockedByETP: true, + }, + { + id: "BmAuth", + platform: "all", + branches: ["nightly"], + name: "BmAuth by 9c9media", + bug: "1486337", + file: "bmauth.js", + matches: ["*://auth.9c9media.ca/auth/main.js"], + onlyIfBlockedByETP: true, + }, + { + id: "Eluminate", + platform: "all", + branches: ["nightly"], + name: "Eluminate", + bug: "1503211", + file: "eluminate.js", + matches: ["*://libs.coremetrics.com/eluminate.js"], + onlyIfBlockedByETP: true, + }, + { + id: "FacebookSDK", + platform: "all", + branches: ["nightly"], + name: "Facebook SDK", + bug: "1226498", + file: "facebook-sdk.js", + matches: [ + "*://connect.facebook.net/*/sdk.js*", + "*://connect.facebook.net/*/all.js*", + ], + needsShimHelpers: ["optIn"], + onlyIfBlockedByETP: true, + unblocksOnOptIn: [ + "*://*.xx.fbcdn.net/*", // covers: + // "*://scontent-.*-\d.xx.fbcdn.net/*", + // "*://static.xx.fbcdn.net/rsrc.php/*", + + "*://www.facebook.com/plugins/comments.php*", + "*://www.facebook.com/plugins/comments/async/*", + "*://www.facebook.com/plugins/feedback.php*", + "*://www.facebook.com/plugins/like_box.php*", + ], + }, + { + id: "GoogleAnalytics", + platform: "all", + branches: ["nightly"], + name: "Google Analytics", + bug: "1493602", + file: "google-analytics.js", + matches: ["*://www.google-analytics.com/analytics.js"], + onlyIfBlockedByETP: true, + }, + { + id: "GoogleAnalyticsECommercePlugin", + platform: "all", + branches: ["nightly"], + name: "Google Analytics E-Commerce Plugin", + bug: "1620533", + file: "google-analytics-ecommerce-plugin.js", + matches: ["*://www.google-analytics.com/plugins/ua/ec.js"], + onlyIfBlockedByETP: true, + }, + { + id: "GoogleAnalyticsTagManager", + platform: "all", + branches: ["nightly"], + name: "Google Analytics Tag Manager", + bug: "1478593", + file: "google-analytics-tag-manager.js", + matches: ["*://www.google-analytics.com/gtm/js"], + onlyIfBlockedByETP: true, + }, + { + id: "GoogleAnalyticsLegacy", + platform: "all", + branches: ["nightly"], + name: "Legacy Google Analytics", + bug: "1487072", + file: "google-analytics-legacy.js", + matches: ["*://ssl.google-analytics.com/ga.js"], + onlyIfBlockedByETP: true, + }, + { + id: "GooglePublisherTags", + platform: "all", + branches: ["nightly"], + name: "Google Publisher Tags", + bug: "1600538", + file: "google-publisher-tags.js", + matches: [ + "*://www.googletagservices.com/tag/js/gpt.js", + "*://securepubads.g.doubleclick.net/tag/js/gpt.js", + "*://securepubads.g.doubleclick.net/gpt/pubads_impl_*.js", + ], + onlyIfBlockedByETP: true, + unblocksOnOptIn: ["*://pubads.g.doubleclick.net/ssai/event/*/streams"], + }, + { + id: "IMA3", + platform: "all", + branches: ["nightly"], + name: "IMA3", + bug: "1487373", + file: "empty-script.js", + onlyIfBlockedByETP: true, + matches: ["*://s0.2mdn.net/instream/html5/ima3.js"], + }, + { + id: "Rambler", + platform: "all", + branches: ["nightly"], + name: "Rambler Authenticator", + bug: "1606428", + file: "rambler-authenticator.js", + matches: ["*://id.rambler.ru/rambler-id-helper/auth_events.js"], + needsShimHelpers: ["optIn"], + onlyIfBlockedByETP: true, + }, + { + id: "RichRelevance", + platform: "all", + branches: ["nightly"], + name: "Rich Relevance", + bug: "1449347", + file: "rich-relevance.js", + matches: ["*://media.richrelevance.com/rrserver/js/1.2/p13n.js"], + onlyIfBlockedByETP: true, + }, +]; + +module.exports = AVAILABLE_SHIMS; diff --git a/browser/extensions/webcompat/data/ua_overrides.js b/browser/extensions/webcompat/data/ua_overrides.js new file mode 100644 index 0000000000..daa947989c --- /dev/null +++ b/browser/extensions/webcompat/data/ua_overrides.js @@ -0,0 +1,686 @@ +/* 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, require */ + +// This is a hack for the tests. +if (typeof InterventionHelpers === "undefined") { + var InterventionHelpers = require("../lib/intervention_helpers"); +} + +/** + * For detailed information on our policies, and a documention on this format + * and its possibilites, please check the Mozilla-Wiki at + * + * https://wiki.mozilla.org/Compatibility/Go_Faster_Addon/Override_Policies_and_Workflows#User_Agent_overrides + */ +const AVAILABLE_UA_OVERRIDES = [ + { + id: "testbed-override", + platform: "all", + domain: "webcompat-addon-testbed.herokuapp.com", + bug: "0000000", + config: { + hidden: true, + matches: ["*://webcompat-addon-testbed.herokuapp.com/*"], + uaTransformer: originalUA => { + return ( + UAHelpers.getPrefix(originalUA) + + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36 for WebCompat" + ); + }, + }, + }, + { + /* + * Bug 1577519 - att.tv - Create a UA override for att.tv for playback on desktop + * WebCompat issue #3846 - https://webcompat.com/issues/3846 + * + * att.tv (atttvnow.com) is blocking Firefox via UA sniffing. Spoofing as Chrome allows + * to access the site and playback works fine. This is former directvnow.com + */ + id: "bug1577519", + platform: "desktop", + domain: "att.tv", + bug: "1577519", + config: { + matches: ["*://*.att.tv/*"], + uaTransformer: originalUA => { + return ( + UAHelpers.getPrefix(originalUA) + + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36" + ); + }, + }, + }, + { + /* + * Bug 1570108 - steamcommunity.com - UA override for steamcommunity.com + * WebCompat issue #34171 - https://webcompat.com/issues/34171 + * + * steamcommunity.com blocks chat feature for Firefox users showing unsupported browser message. + * When spoofing as Chrome the chat works fine + */ + id: "bug1570108", + platform: "desktop", + domain: "steamcommunity.com", + bug: "1570108", + config: { + matches: ["*://steamcommunity.com/chat*"], + uaTransformer: originalUA => { + return ( + UAHelpers.getPrefix(originalUA) + + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36" + ); + }, + }, + }, + { + /* + * Bug 1582582 - sling.com - UA override for sling.com + * WebCompat issue #17804 - https://webcompat.com/issues/17804 + * + * sling.com blocks Firefox users showing unsupported browser message. + * When spoofing as Chrome playing content works fine + */ + id: "bug1582582", + platform: "desktop", + domain: "sling.com", + bug: "1582582", + config: { + matches: ["https://watch.sling.com/*", "https://www.sling.com/*"], + uaTransformer: originalUA => { + return ( + UAHelpers.getPrefix(originalUA) + + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36" + ); + }, + }, + }, + { + /* + * Bug 1610026 - www.mobilesuica.com - UA override for www.mobilesuica.com + * WebCompat issue #4608 - https://webcompat.com/issues/4608 + * + * mobilesuica.com showing unsupported message for Firefox users + * Spoofing as Chrome allows to access the page + */ + id: "bug1610026", + platform: "all", + domain: "www.mobilesuica.com", + bug: "1610026", + config: { + matches: ["https://www.mobilesuica.com/*"], + uaTransformer: originalUA => { + return ( + UAHelpers.getPrefix(originalUA) + + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36" + ); + }, + }, + }, + { + /* + * Bug 945963 - tieba.baidu.com serves simplified mobile content to Firefox Android + * additionally, Bug 1525839 for more domains + * WebCompat issue #18455 - https://webcompat.com/issues/18455 + * + * tieba.baidu.com and tiebac.baidu.com serve a heavily simplified and less functional + * mobile experience to Firefox for Android users. Adding the AppleWebKit indicator + * to the User Agent gets us the same experience. + */ + id: "bug945963", + platform: "android", + domain: "tieba.baidu.com", + bug: "945963", + config: { + matches: [ + "*://baike.baidu.com/*", + "*://image.baidu.com/*", + "*://news.baidu.com/*", + "*://tieba.baidu.com/*", + "*://tiebac.baidu.com/*", + "*://wenku.baidu.com/*", + "*://zhidao.baidu.com/*", + ], + uaTransformer: originalUA => { + return UAHelpers.getDeviceAppropriateChromeUA(); + }, + }, + }, + { + /* + * Bug 1177298 - Write UA overrides for top Japanese Sites + * (Imported from ua-update.json.in) + * + * To receive the proper mobile version instead of the desktop version or + * a lower grade mobile experience, the UA is spoofed. + */ + id: "bug1177298-2", + platform: "android", + domain: "lohaco.jp", + bug: "1177298", + config: { + matches: ["*://*.lohaco.jp/*"], + uaTransformer: _ => { + return "Mozilla/5.0 (Linux; Android 5.0.2; Galaxy Nexus Build/IMM76B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.93 Mobile Safari/537.36"; + }, + }, + }, + { + /* + * Bug 1177298 - Write UA overrides for top Japanese Sites + * (Imported from ua-update.json.in) + * + * To receive the proper mobile version instead of the desktop version or + * a lower grade mobile experience, the UA is spoofed. + */ + id: "bug1177298-3", + platform: "android", + domain: "nhk.or.jp", + bug: "1177298", + config: { + matches: ["*://*.nhk.or.jp/*"], + uaTransformer: originalUA => { + return originalUA + " AppleWebKit"; + }, + }, + }, + { + /* + * Bug 1385206 - Create UA override for rakuten.co.jp on Firefox Android + * (Imported from ua-update.json.in) + * + * rakuten.co.jp serves a Desktop version if Firefox is included in the UA. + */ + id: "bug1385206", + platform: "android", + domain: "rakuten.co.jp", + bug: "1385206", + config: { + matches: ["*://*.rakuten.co.jp/*"], + uaTransformer: originalUA => { + return originalUA.replace(/Firefox.+$/, ""); + }, + }, + }, + { + /* + * Bug 969844 - mobile.de sends desktop site to Firefox on Android + * + * mobile.de sends the desktop site to Firefox Mobile. + * Spoofing as Chrome works fine. + */ + id: "bug969844", + platform: "android", + domain: "mobile.de", + bug: "969844", + config: { + matches: ["*://*.mobile.de/*"], + uaTransformer: _ => { + return "Mozilla/5.0 (Linux; Android 6.0.1; SM-G920F Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36"; + }, + }, + }, + { + /* + * Bug 1509831 - cc.com - Add UA override for CC.com + * WebCompat issue #329 - https://webcompat.com/issues/329 + * + * ComedyCentral blocks Firefox for not being able to play HLS, which was + * true in previous versions, but no longer is. With a spoofed Chrome UA, + * the site works just fine. + */ + id: "bug1509831", + platform: "android", + domain: "cc.com", + bug: "1509831", + config: { + matches: ["*://*.cc.com/*"], + uaTransformer: _ => { + return "Mozilla/5.0 (Linux; Android 6.0.1; SM-G920F Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36"; + }, + }, + }, + { + /* + * Bug 1509873 - zmags.com - Add UA override for secure.viewer.zmags.com + * WebCompat issue #21576 - https://webcompat.com/issues/21576 + * + * The zmags viewer locks out Firefox Mobile with a "Browser unsupported" + * message, but tests showed that it works just fine with a Chrome UA. + * Outreach attempts were unsuccessful, and as the site has a relatively + * high rank, we alter the UA. + */ + id: "bug1509873", + platform: "android", + domain: "zmags.com", + bug: "1509873", + config: { + matches: ["*://*.viewer.zmags.com/*"], + uaTransformer: originalUA => { + return ( + UAHelpers.getPrefix(originalUA) + + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36" + ); + }, + }, + }, + { + /* + * Bug 1566253 - posts.google.com - Add UA override for posts.google.com + * WebCompat issue #17870 - https://webcompat.com/issues/17870 + * + * posts.google.com displaying "Your browser doesn't support this page". + * Spoofing as Chrome works fine. + */ + id: "bug1566253", + platform: "android", + domain: "posts.google.com", + bug: "1566253", + config: { + matches: ["*://posts.google.com/*"], + uaTransformer: _ => { + return "Mozilla/5.0 (Linux; Android 6.0.1; SM-G900M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.101 Mobile Safari/537.36"; + }, + }, + }, + { + /* + * Bug 1574522 - UA override for enuri.com on Firefox for Android + * WebCompat issue #37139 - https://webcompat.com/issues/37139 + * + * enuri.com returns a different template for Firefox on Android + * based on server side UA detection. This results in page content cut offs. + * Spoofing as Chrome fixes the issue + */ + id: "bug1574522", + platform: "android", + domain: "enuri.com", + bug: "1574522", + config: { + matches: ["*://enuri.com/*"], + uaTransformer: _ => { + return "Mozilla/5.0 (Linux; Android 6.0.1; SM-G900M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.111 Mobile Safari/537.36"; + }, + }, + }, + { + /* + * Bug 1574564 - UA override for ceskatelevize.cz on Firefox for Android + * WebCompat issue #15467 - https://webcompat.com/issues/15467 + * + * ceskatelevize sets streamingProtocol depending on the User-Agent it sees + * in the request headers, returning DASH for Chrome, HLS for iOS, + * and Flash for Firefox Mobile. Since Mobile has no Flash, the video + * doesn't work. Spoofing as Chrome makes the video play + */ + id: "bug1574564", + platform: "android", + domain: "ceskatelevize.cz", + bug: "1574564", + config: { + matches: ["*://*.ceskatelevize.cz/*"], + uaTransformer: originalUA => { + return ( + UAHelpers.getPrefix(originalUA) + + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.111 Mobile Safari/537.36" + ); + }, + }, + }, + { + /* + * Bug 1577250 - UA override for homebook.pl on Firefox for Android + * WebCompat issue #24044 - https://webcompat.com/issues/24044 + * + * homebook.pl shows desktop site on Firefox for Android based on + * UA detection. Spoofing as Chrome allows to get mobile site. + */ + id: "bug1577250", + platform: "android", + domain: "homebook.pl", + bug: "1577250", + config: { + matches: ["*://*.homebook.pl/*"], + uaTransformer: originalUA => { + return ( + UAHelpers.getPrefix(originalUA) + + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.111 Mobile Safari/537.36" + ); + }, + }, + }, + { + /* + * Bug 1577267 - UA override for metfone.com.kh on Firefox for Android + * WebCompat issue #16363 - https://webcompat.com/issues/16363 + * + * metfone.com.kh has a server side UA detection which returns desktop site + * for Firefox for Android. Spoofing as Chrome allows to receive mobile version + */ + id: "bug1577267", + platform: "android", + domain: "metfone.com.kh", + bug: "1577267", + config: { + matches: ["*://*.metfone.com.kh/*"], + uaTransformer: originalUA => { + return ( + UAHelpers.getPrefix(originalUA) + + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.111 Mobile Safari/537.36" + ); + }, + }, + }, + { + /* + * Bug 1598198 - User Agent extension for Samsung's galaxy.store URLs + * + * Samsung's galaxy.store shortlinks are supposed to redirect to a Samsung + * intent:// URL on Samsung devices, but to an error page on other brands. + * As we do not provide device info in our user agent string, this check + * fails, and even Samsung users land on an error page if they use Firefox + * for Android. + * This intervention adds a simple "Samsung" identifier to the User Agent + * on only the Galaxy Store URLs if the device happens to be a Samsung. + */ + id: "bug1598198", + platform: "android", + domain: "galaxy.store", + bug: "1598198", + config: { + matches: [ + "*://galaxy.store/*", + "*://dev.galaxy.store/*", + "*://stg.galaxy.store/*", + ], + uaTransformer: originalUA => { + if (!browser.systemManufacturer) { + return originalUA; + } + + const manufacturer = browser.systemManufacturer.getManufacturer(); + if (manufacturer && manufacturer.toLowerCase() === "samsung") { + return originalUA.replace("Mobile;", "Mobile; Samsung;"); + } + + return originalUA; + }, + }, + }, + { + /* + * Bug 1595215 - UA overrides for Uniqlo sites + * Webcompat issue #38825 - https://webcompat.com/issues/38825 + * + * To receive the proper mobile version instead of the desktop version or + * avoid redirect loop, the UA is spoofed. + */ + id: "bug1595215", + platform: "android", + domain: "uniqlo.com", + bug: "1595215", + config: { + matches: ["*://*.uniqlo.com/*"], + uaTransformer: originalUA => { + return originalUA + " Mobile Safari"; + }, + }, + }, + { + /* + * Bug 1621065 - UA overrides for bracketchallenge.ncaa.com + * Webcompat issue #49886 - https://webcompat.com/issues/49886 + * + * The NCAA bracket challenge website mistakenly classifies + * any non-Chrome browser on Android as "is_old_android". As a result, + * a modal is shown telling them they have security flaws. We have + * attempted to reach out for a fix (and clarification). + */ + id: "bug1621065", + platform: "android", + domain: "bracketchallenge.ncaa.com", + bug: "1621065", + config: { + matches: ["*://bracketchallenge.ncaa.com/*"], + uaTransformer: originalUA => { + return originalUA + " Chrome"; + }, + }, + }, + { + /* + * Bug 1622063 - UA override for wp1-ext.usps.gov + * Webcompat issue #29867 - https://webcompat.com/issues/29867 + * + * The Job Search site for USPS does not work for Firefox Mobile + * browsers (a 500 is returned). + */ + id: "bug1622063", + platform: "android", + domain: "wp1-ext.usps.gov", + bug: "1622063", + config: { + matches: ["*://wp1-ext.usps.gov/*"], + uaTransformer: originalUA => { + return UAHelpers.getDeviceAppropriateChromeUA(); + }, + }, + }, + { + /* + * Bug 1622081 - UA override for m2.bmo.com + * Webcompat issue #45019 - https://webcompat.com/issues/45019 + * + * Unless the UA string contains "Chrome", m2.bmo.com will + * display a modal saying the browser is out-of-date. + */ + id: "bug1622081", + platform: "android", + domain: "m2.bmo.com", + bug: "1622081", + config: { + matches: ["*://m2.bmo.com/*"], + uaTransformer: originalUA => { + return originalUA + " Chrome"; + }, + }, + }, + { + /* + * Bug 1628455 - UA override for autotrader.ca + * Webcompat issue #50961 - https://webcompat.com/issues/50961 + * + * autotrader.ca is showing desktop site for Firefox on Android + * based on server side UA detection. Spoofing as Chrome allows to + * get mobile experience + */ + id: "bug1628455", + platform: "android", + domain: "autotrader.ca", + bug: "1628455", + config: { + matches: ["https://*.autotrader.ca/*"], + uaTransformer: () => { + return UAHelpers.getDeviceAppropriateChromeUA(); + }, + }, + }, + { + /* + * Bug 1630280 - UA override for dominos.ch + * Webcompat issue #48273 - https://webcompat.com/issues/48273 + * + * dominos.ch is suggesting downloading their native app and showing + * an overlay that can't be removed in Firefox for Android. Spoofing + * as Chrome allows to continue to the site + */ + id: "bug1630280", + platform: "android", + domain: "dominos.ch", + bug: "1630280", + config: { + matches: ["https://*.dominos.ch/*"], + uaTransformer: () => { + return UAHelpers.getDeviceAppropriateChromeUA(); + }, + }, + }, + { + /* + * Bug 1563839 - rolb.santanderbank.com - Build UA override + * Bug 1646791 - bancosantander.es - Re-add UA override. + * Bug 1665129 - *.gruposantander.es - Add wildcard domains. + * WebCompat issue #33462 - https://webcompat.com/issues/33462 + * SuMo request - https://support.mozilla.org/es/questions/1291085 + * + * santanderbank expects UA to have 'like Gecko', otherwise it runs + * xmlDoc.onload whose support has been dropped. It results in missing labels in forms + * and some other issues. Adding 'like Gecko' fixes those issues. + */ + id: "bug1646791", + platform: "all", + domain: "santanderbank.com", + bug: "1646791", + config: { + matches: [ + "*://*.bancosantander.es/*", + "*://*.gruposantander.es/*", + "*://*.santander.co.uk/*", + "*://bob.santanderbank.com/*", + "*://rolb.santanderbank.com/*", + ], + uaTransformer: originalUA => { + return originalUA.replace("Gecko", "like Gecko"); + }, + }, + }, + { + /* + * Bug 1651292 - UA override for www.jp.square-enix.com + * Webcompat issue #53018 - https://webcompat.com/issues/53018 + * + * Unless the UA string contains "Chrome 66+", a section of + * www.jp.square-enix.com will show a never ending LOADING + * page. + */ + id: "bug1651292", + platform: "android", + domain: "www.jp.square-enix.com", + bug: "1651292", + config: { + matches: ["*://www.jp.square-enix.com/music/sem/page/FF7R/ost/*"], + uaTransformer: originalUA => { + return originalUA + " Chrome/83"; + }, + }, + }, + { + /* + * Bug 1654888 - UA override for ebuyer.com + * Webcompat issue #52463 - https://webcompat.com/issues/52463 + * + * This site returns desktop site based on server side UA detection. + * Spoofing as Chrome allows to get mobile experience + */ + id: "bug1654888", + platform: "android", + domain: "ebuyer.com", + bug: "1654888", + config: { + matches: ["*://*.ebuyer.com/*"], + uaTransformer: () => { + return UAHelpers.getDeviceAppropriateChromeUA(); + }, + }, + }, + { + /* + * Bug 1666754 - Mobile UA override for lffl.org + * Bug 1665720 - lffl.org article page takes 2x as much time to load on Moto G + * + * This site returns desktop site based on server side UA detection. + * Spoofing as Chrome allows to get mobile experience + */ + id: "bug1666754", + platform: "android", + domain: "lffl.org", + bug: "1666754", + config: { + matches: ["*://*.lffl.org/*"], + uaTransformer: () => { + return UAHelpers.getDeviceAppropriateChromeUA(); + }, + }, + }, + { + /* + * Bug 1679847 - Add UA override for avto.pro + * Webcompat issue #60043 - https://webcompat.com/issues/60043 + * + * Unless Chrome is in the UA, the site serves a desktop version + * on catalog pages + */ + id: "bug1679847", + platform: "android", + domain: "avto.pro", + bug: "1679847", + config: { + matches: ["https://avto.pro/catalog/*"], + uaTransformer: () => { + return UAHelpers.getDeviceAppropriateChromeUA(); + }, + }, + }, + { + /* + * Bug 1679869 - Add UA override for vh1.com + * Webcompat issue #52755 - https://webcompat.com/issues/52755 + * + * The site is not showing videos on Firefox on mobile stating + * that android 4.4.4 and chrome browser required + */ + id: "bug1679869", + platform: "android", + domain: "vh1.com", + bug: "1679869", + config: { + matches: ["*://*.vh1.com/*"], + uaTransformer: () => { + return UAHelpers.getDeviceAppropriateChromeUA(); + }, + }, + }, +]; + +const UAHelpers = { + getDeviceAppropriateChromeUA() { + if (!UAHelpers._deviceAppropriateChromeUA) { + const userAgent = + typeof navigator !== "undefined" ? navigator.userAgent : ""; + const RunningFirefoxVersion = (userAgent.match(/Firefox\/([0-9.]+)/) || [ + "", + "58.0", + ])[1]; + const RunningAndroidVersion = + userAgent.match(/Android\/[0-9.]+/) || "Android 6.0"; + const ChromeVersionToMimic = "76.0.3809.111"; + const ChromePhoneUA = `Mozilla/5.0 (Linux; ${RunningAndroidVersion}; Nexus 5 Build/MRA58N) FxQuantum/${RunningFirefoxVersion} AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${ChromeVersionToMimic} Mobile Safari/537.36`; + const ChromeTabletUA = `Mozilla/5.0 (Linux; ${RunningAndroidVersion}; Nexus 7 Build/JSS15Q) FxQuantum/${RunningFirefoxVersion} AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${ChromeVersionToMimic} Safari/537.36`; + const IsPhone = userAgent.includes("Mobile"); + UAHelpers._deviceAppropriateChromeUA = IsPhone + ? ChromePhoneUA + : ChromeTabletUA; + } + return UAHelpers._deviceAppropriateChromeUA; + }, + getPrefix(originalUA) { + return originalUA.substr(0, originalUA.indexOf(")") + 1); + }, +}; + +module.exports = AVAILABLE_UA_OVERRIDES; diff --git a/browser/extensions/webcompat/experiment-apis/aboutConfigPrefs.js b/browser/extensions/webcompat/experiment-apis/aboutConfigPrefs.js new file mode 100644 index 0000000000..08a4c3d091 --- /dev/null +++ b/browser/extensions/webcompat/experiment-apis/aboutConfigPrefs.js @@ -0,0 +1,57 @@ +/* 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"; + +/* global ExtensionAPI, ExtensionCommon, Services, XPCOMUtils */ + +XPCOMUtils.defineLazyModuleGetters(this, { + Services: "resource://gre/modules/Services.jsm", +}); + +this.aboutConfigPrefs = class extends ExtensionAPI { + getAPI(context) { + const EventManager = ExtensionCommon.EventManager; + const extensionIDBase = context.extension.id.split("@")[0]; + const extensionPrefNameBase = `extensions.${extensionIDBase}.`; + + return { + aboutConfigPrefs: { + onPrefChange: new EventManager({ + context, + name: "aboutConfigPrefs.onUAOverridesPrefChange", + register: (fire, name) => { + const prefName = `${extensionPrefNameBase}${name}`; + const callback = () => { + fire.async(name).catch(() => {}); // ignore Message Manager disconnects + }; + Services.prefs.addObserver(prefName, callback); + return () => { + Services.prefs.removeObserver(prefName, callback); + }; + }, + }).api(), + async getBranch(branchName) { + const branch = `${extensionPrefNameBase}${branchName}.`; + return Services.prefs.getChildList(branch).map(pref => { + const name = pref.replace(branch, ""); + return { name, value: Services.prefs.getBoolPref(pref) }; + }); + }, + async getPref(name) { + try { + return Services.prefs.getBoolPref( + `${extensionPrefNameBase}${name}` + ); + } catch (_) { + return undefined; + } + }, + async setPref(name, value) { + Services.prefs.setBoolPref(`${extensionPrefNameBase}${name}`, value); + }, + }, + }; + } +}; diff --git a/browser/extensions/webcompat/experiment-apis/aboutConfigPrefs.json b/browser/extensions/webcompat/experiment-apis/aboutConfigPrefs.json new file mode 100644 index 0000000000..44284f199c --- /dev/null +++ b/browser/extensions/webcompat/experiment-apis/aboutConfigPrefs.json @@ -0,0 +1,72 @@ +[ + { + "namespace": "aboutConfigPrefs", + "description": "experimental API extension to allow access to about:config preferences", + "events": [ + { + "name": "onPrefChange", + "type": "function", + "parameters": [ + { + "name": "name", + "type": "string", + "description": "The preference which changed" + } + ], + "extraParameters": [ + { + "name": "name", + "type": "string", + "description": "The preference to monitor" + } + ] + } + ], + "functions": [ + { + "name": "getBranch", + "type": "function", + "description": "Get all child prefs for a branch", + "parameters": [ + { + "name": "branchName", + "type": "string", + "description": "The branch name" + } + ], + "async": true + }, + { + "name": "getPref", + "type": "function", + "description": "Get a preference's value", + "parameters": [ + { + "name": "name", + "type": "string", + "description": "The preference name" + } + ], + "async": true + }, + { + "name": "setPref", + "type": "function", + "description": "Set a preference's value", + "parameters": [ + { + "name": "name", + "type": "string", + "description": "The preference name" + }, + { + "name": "value", + "type": "boolean", + "description": "The new value" + } + ], + "async": true + } + ] + } +] diff --git a/browser/extensions/webcompat/experiment-apis/appConstants.js b/browser/extensions/webcompat/experiment-apis/appConstants.js new file mode 100644 index 0000000000..7019eb6215 --- /dev/null +++ b/browser/extensions/webcompat/experiment-apis/appConstants.js @@ -0,0 +1,32 @@ +/* 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"; + +/* global ExtensionAPI, XPCOMUtils */ + +XPCOMUtils.defineLazyModuleGetters(this, { + AppConstants: "resource://gre/modules/AppConstants.jsm", +}); + +this.appConstants = class extends ExtensionAPI { + getAPI(context) { + return { + appConstants: { + getReleaseBranch: () => { + if (AppConstants.NIGHTLY_BUILD) { + return "nightly"; + } else if (AppConstants.MOZ_DEV_EDITION) { + return "dev_edition"; + } else if (AppConstants.EARLY_BETA_OR_EARLIER) { + return "early_beta_or_earlier"; + } else if (AppConstants.BETA_OR_RELEASE) { + return "beta_or_release"; + } + return "unknown"; + }, + }, + }; + } +}; diff --git a/browser/extensions/webcompat/experiment-apis/appConstants.json b/browser/extensions/webcompat/experiment-apis/appConstants.json new file mode 100644 index 0000000000..cf04915eca --- /dev/null +++ b/browser/extensions/webcompat/experiment-apis/appConstants.json @@ -0,0 +1,15 @@ +[ + { + "namespace": "appConstants", + "description": "experimental API to expose some app constants", + "functions": [ + { + "name": "getReleaseBranch", + "type": "function", + "description": "", + "async": true, + "parameters": [] + } + ] + } +] diff --git a/browser/extensions/webcompat/experiment-apis/experiments.js b/browser/extensions/webcompat/experiment-apis/experiments.js new file mode 100644 index 0000000000..d1ab77c312 --- /dev/null +++ b/browser/extensions/webcompat/experiment-apis/experiments.js @@ -0,0 +1,34 @@ +/* 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"; + +/* global ExtensionAPI, Services, XPCOMUtils */ + +XPCOMUtils.defineLazyModuleGetters(this, { + EventDispatcher: "resource://gre/modules/Messaging.jsm", + Services: "resource://gre/modules/Services.jsm", +}); + +this.experiments = class extends ExtensionAPI { + getAPI(context) { + function promiseActiveExperiments() { + return EventDispatcher.instance.sendRequestForResult({ + type: "Experiments:GetActive", + }); + } + return { + experiments: { + async isActive(name) { + if (!Services.androidBridge || !Services.androidBridge.isFennec) { + return undefined; + } + return promiseActiveExperiments().then(experiments => { + return experiments.includes(name); + }); + }, + }, + }; + } +}; diff --git a/browser/extensions/webcompat/experiment-apis/experiments.json b/browser/extensions/webcompat/experiment-apis/experiments.json new file mode 100644 index 0000000000..44f833215c --- /dev/null +++ b/browser/extensions/webcompat/experiment-apis/experiments.json @@ -0,0 +1,21 @@ +[ + { + "namespace": "experiments", + "description": "experimental API extension to allow checking the status of Fennec experiments via Switchboard", + "functions": [ + { + "name": "isActive", + "type": "function", + "description": "Determine if a given experiment is active.", + "parameters": [ + { + "name": "name", + "type": "string", + "description": "The experiment's name" + } + ], + "async": true + } + ] + } +] diff --git a/browser/extensions/webcompat/experiment-apis/matchPatterns.js b/browser/extensions/webcompat/experiment-apis/matchPatterns.js new file mode 100644 index 0000000000..422cba5fc4 --- /dev/null +++ b/browser/extensions/webcompat/experiment-apis/matchPatterns.js @@ -0,0 +1,30 @@ +/* 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"; + +/* global ExtensionAPI */ + +this.matchPatterns = class extends ExtensionAPI { + getAPI(context) { + return { + matchPatterns: { + getMatcher(patterns) { + const set = new MatchPatternSet(patterns); + return Cu.cloneInto( + { + matches: url => { + return set.matches(url); + }, + }, + context.cloneScope, + { + cloneFunctions: true, + } + ); + }, + }, + }; + } +}; diff --git a/browser/extensions/webcompat/experiment-apis/matchPatterns.json b/browser/extensions/webcompat/experiment-apis/matchPatterns.json new file mode 100644 index 0000000000..6fb4dc10fc --- /dev/null +++ b/browser/extensions/webcompat/experiment-apis/matchPatterns.json @@ -0,0 +1,29 @@ +[ + { + "namespace": "matchPatterns", + "description": "experimental API extension to expose MatchPattern functionality", + "functions": [ + { + "name": "getMatcher", + "type": "function", + "description": "get a MatchPatternSet", + "parameters": [ + { + "name": "patterns", + "description": "Array of string URL patterns to match", + "type": "array", + "items": { + "type": "string" + } + } + ], + "returns": { + "type": "object", + "properties": { + "matches": { "type": "function" } + } + } + } + ] + } +] diff --git a/browser/extensions/webcompat/experiment-apis/pictureInPicture.js b/browser/extensions/webcompat/experiment-apis/pictureInPicture.js new file mode 100644 index 0000000000..e78ddbf315 --- /dev/null +++ b/browser/extensions/webcompat/experiment-apis/pictureInPicture.js @@ -0,0 +1,90 @@ +/* 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"; + +/* global ChromeUtils, ExtensionAPI, Services */ +ChromeUtils.defineModuleGetter( + this, + "Services", + "resource://gre/modules/Services.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "KEYBOARD_CONTROLS", + "resource://gre/modules/PictureInPictureControls.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "TOGGLE_POLICIES", + "resource://gre/modules/PictureInPictureControls.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "AppConstants", + "resource://gre/modules/AppConstants.jsm" +); + +const TOGGLE_ENABLED_PREF = + "media.videocontrols.picture-in-picture.video-toggle.enabled"; + +/** + * This API is expected to be running in the parent process. + */ +this.pictureInPictureParent = class extends ExtensionAPI { + getAPI(context) { + return { + pictureInPictureParent: { + setOverrides(overrides) { + // The Picture-in-Picture toggle is only implemented for Desktop, so make + // this a no-op for non-Desktop builds. + if (AppConstants.platform == "android") { + return; + } + + Services.ppmm.sharedData.set( + "PictureInPicture:SiteOverrides", + overrides + ); + }, + }, + }; + } +}; + +/** + * This API is expected to be running in a content process - specifically, + * the WebExtension content process that the background scripts run in. We + * split these out so that they can return values synchronously to the + * background scripts. + */ +this.pictureInPictureChild = class extends ExtensionAPI { + getAPI(context) { + return { + pictureInPictureChild: { + getKeyboardControls() { + // The Picture-in-Picture toggle is only implemented for Desktop, so make + // this return nothing for non-Desktop builds. + if (AppConstants.platform == "android") { + return Cu.cloneInto({}, context.cloneScope); + } + + return Cu.cloneInto(KEYBOARD_CONTROLS, context.cloneScope); + }, + getPolicies() { + // The Picture-in-Picture toggle is only implemented for Desktop, so make + // this return nothing for non-Desktop builds. + if (AppConstants.platform == "android") { + return Cu.cloneInto({}, context.cloneScope); + } + + return Cu.cloneInto(TOGGLE_POLICIES, context.cloneScope); + }, + }, + }; + } +}; diff --git a/browser/extensions/webcompat/experiment-apis/pictureInPicture.json b/browser/extensions/webcompat/experiment-apis/pictureInPicture.json new file mode 100644 index 0000000000..5f34616b6e --- /dev/null +++ b/browser/extensions/webcompat/experiment-apis/pictureInPicture.json @@ -0,0 +1,51 @@ +[ + { + "namespace": "pictureInPictureParent", + "description": "Parent process methods for controlling the Picture-in-Picture feature.", + "functions": [ + { + "name": "setOverrides", + "type": "function", + "description": "Set Picture-in-Picture toggle position overrides", + "parameters": [ + { + "name": "overrides", + "type": "object", + "additionalProperties": { "type": "any" }, + "description": "The Picture-in-Picture toggle position overrides to set" + } + ] + } + ] + }, + { + "namespace": "pictureInPictureChild", + "description": "WebExtension process methods for querying the Picture-in-Picture feature.", + "functions": [ + { + "name": "getKeyboardControls", + "type": "function", + "description": "Get the Picture-in-Picture keyboard control override constants", + "parameters": [], + "returns": { + "type": "object", + "properties": {}, + "additionalProperties": { "type": "any" }, + "description": "The Picture-in-Picture keyboard control override constants" + } + }, + { + "name": "getPolicies", + "type": "function", + "description": "Get the Picture-in-Picture toggle position override constants", + "parameters": [], + "returns": { + "type": "object", + "properties": {}, + "additionalProperties": { "type": "any" }, + "description": "The Picture-in-Picture toggle position override constants" + } + } + ] + } +] diff --git a/browser/extensions/webcompat/experiment-apis/sharedPreferences.js b/browser/extensions/webcompat/experiment-apis/sharedPreferences.js new file mode 100644 index 0000000000..b2a19b3a6c --- /dev/null +++ b/browser/extensions/webcompat/experiment-apis/sharedPreferences.js @@ -0,0 +1,33 @@ +/* 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"; + +/* global ExtensionAPI, Services, XPCOMUtils */ + +XPCOMUtils.defineLazyModuleGetters(this, { + Services: "resource://gre/modules/Services.jsm", + SharedPreferences: "resource://gre/modules/SharedPreferences.jsm", +}); + +this.sharedPreferences = class extends ExtensionAPI { + getAPI(context) { + return { + sharedPreferences: { + async setCharPref(name, value) { + if (!Services.androidBridge || !Services.androidBridge.isFennec) { + return; + } + SharedPreferences.forApp().setCharPref(name, value); + }, + async setBoolPref(name, value) { + if (!Services.androidBridge || !Services.androidBridge.isFennec) { + return; + } + SharedPreferences.forApp().setBoolPref(name, value); + }, + }, + }; + } +}; diff --git a/browser/extensions/webcompat/experiment-apis/sharedPreferences.json b/browser/extensions/webcompat/experiment-apis/sharedPreferences.json new file mode 100644 index 0000000000..cf73414cf4 --- /dev/null +++ b/browser/extensions/webcompat/experiment-apis/sharedPreferences.json @@ -0,0 +1,44 @@ +[ + { + "namespace": "sharedPreferences", + "description": "experimental API extension to allow setting SharedPreferences on Fennec", + "functions": [ + { + "name": "setBoolPref", + "type": "function", + "description": "Set the value of a boolean Fennec SharedPreference", + "parameters": [ + { + "name": "name", + "type": "string", + "description": "The key name" + }, + { + "name": "value", + "type": "boolean", + "description": "The new value" + } + ], + "async": true + }, + { + "name": "setCharPref", + "type": "function", + "description": "Set the value of a string Fennec SharedPreference", + "parameters": [ + { + "name": "name", + "type": "string", + "description": "The key name" + }, + { + "name": "value", + "type": "string", + "description": "The new value" + } + ], + "async": true + } + ] + } +] diff --git a/browser/extensions/webcompat/experiment-apis/systemManufacturer.js b/browser/extensions/webcompat/experiment-apis/systemManufacturer.js new file mode 100644 index 0000000000..c3819c1128 --- /dev/null +++ b/browser/extensions/webcompat/experiment-apis/systemManufacturer.js @@ -0,0 +1,27 @@ +/* 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"; + +/* global ExtensionAPI, Services, XPCOMUtils */ + +XPCOMUtils.defineLazyModuleGetters(this, { + Services: "resource://gre/modules/Services.jsm", +}); + +this.systemManufacturer = class extends ExtensionAPI { + getAPI(context) { + return { + systemManufacturer: { + getManufacturer() { + try { + return Services.sysinfo.getProperty("manufacturer"); + } catch (_) { + return undefined; + } + }, + }, + }; + } +}; diff --git a/browser/extensions/webcompat/experiment-apis/systemManufacturer.json b/browser/extensions/webcompat/experiment-apis/systemManufacturer.json new file mode 100644 index 0000000000..c64fccc46d --- /dev/null +++ b/browser/extensions/webcompat/experiment-apis/systemManufacturer.json @@ -0,0 +1,20 @@ +[ + { + "namespace": "systemManufacturer", + "description": "experimental API extension to allow reading the device's manufacturer", + "functions": [ + { + "name": "getManufacturer", + "type": "function", + "description": "Get the device's manufacturer", + "parameters": [], + "returns": { + "type": "string", + "properties": {}, + "additionalProperties": { "type": "any" }, + "description": "The manufacturer's name." + } + } + ] + } +] diff --git a/browser/extensions/webcompat/experiment-apis/trackingProtection.js b/browser/extensions/webcompat/experiment-apis/trackingProtection.js new file mode 100644 index 0000000000..557090e974 --- /dev/null +++ b/browser/extensions/webcompat/experiment-apis/trackingProtection.js @@ -0,0 +1,168 @@ +/* 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"; + +/* global ExtensionAPI, ExtensionCommon, ExtensionParent, Services, XPCOMUtils */ + +XPCOMUtils.defineLazyModuleGetters(this, { + Services: "resource://gre/modules/Services.jsm", +}); + +XPCOMUtils.defineLazyGlobalGetters(this, ["URL", "ChannelWrapper"]); + +class Manager { + constructor() { + this._allowLists = new Map(); + } + + _ensureStarted() { + if (this._classifierObserver) { + return; + } + + this._unblockedChannelIds = new Set(); + this._channelClassifier = Cc[ + "@mozilla.org/url-classifier/channel-classifier-service;1" + ].getService(Ci.nsIChannelClassifierService); + this._classifierObserver = {}; + this._classifierObserver.observe = (subject, topic, data) => { + switch (topic) { + case "http-on-stop-request": { + const { channelId } = subject.QueryInterface(Ci.nsIIdentChannel); + this._unblockedChannelIds.delete(channelId); + break; + } + case "urlclassifier-before-block-channel": { + const channel = subject.QueryInterface( + Ci.nsIUrlClassifierBlockedChannel + ); + const { channelId, url } = channel; + let topHost; + try { + topHost = new URL(channel.topLevelUrl).hostname; + } catch (_) { + return; + } + for (const allowList of this._allowLists.values()) { + for (const entry of allowList.values()) { + const { matcher, hosts, notHosts } = entry; + if (matcher.matches(url)) { + if ( + !notHosts?.has(topHost) && + (hosts === true || hosts.has(topHost)) + ) { + this._unblockedChannelIds.add(channelId); + channel.unblock(); + return; + } + } + } + } + break; + } + } + }; + Services.obs.addObserver(this._classifierObserver, "http-on-stop-request"); + this._channelClassifier.addListener(this._classifierObserver); + } + + stop() { + if (!this._classifierObserver) { + return; + } + + Services.obs.removeObserver( + this._classifierObserver, + "http-on-stop-request" + ); + this._channelClassifier.removeListener(this._classifierObserver); + delete this._channelClassifier; + delete this._classifierObserver; + } + + wasChannelIdUnblocked(channelId) { + return this._unblockedChannelIds.has(channelId); + } + + allow(allowListId, patterns, { hosts, notHosts }) { + this._ensureStarted(); + + if (!this._allowLists.has(allowListId)) { + this._allowLists.set(allowListId, new Map()); + } + const allowList = this._allowLists.get(allowListId); + for (const pattern of patterns) { + if (!allowList.has(pattern)) { + allowList.set(pattern, { + matcher: new MatchPattern(pattern), + }); + } + const allowListPattern = allowList.get(pattern); + if (!hosts) { + allowListPattern.hosts = true; + } else { + if (!allowListPattern.hosts) { + allowListPattern.hosts = new Set(); + } + for (const host of hosts) { + allowListPattern.hosts.add(host); + } + } + if (notHosts) { + if (!allowListPattern.notHosts) { + allowListPattern.notHosts = new Set(); + } + for (const notHost of notHosts) { + allowListPattern.notHosts.add(notHost); + } + } + } + } + + revoke(allowListId) { + this._allowLists.delete(allowListId); + } +} +var manager = new Manager(); + +function getChannelId(context, requestId) { + const wrapper = ChannelWrapper.getRegisteredChannel( + requestId, + context.extension.policy, + context.xulBrowser.frameLoader.remoteTab + ); + return wrapper?.channel?.QueryInterface(Ci.nsIIdentChannel)?.channelId; +} + +this.trackingProtection = class extends ExtensionAPI { + onShutdown(isAppShutdown) { + if (manager) { + manager.stop(); + } + } + + getAPI(context) { + return { + trackingProtection: { + async allow(allowListId, patterns, options) { + manager.allow(allowListId, patterns, options); + }, + async revoke(allowListId) { + manager.revoke(allowListId); + }, + async wasRequestUnblocked(requestId) { + if (!manager) { + return false; + } + const channelId = getChannelId(context, requestId); + if (!channelId) { + return false; + } + return manager.wasChannelIdUnblocked(channelId); + }, + }, + }; + } +}; diff --git a/browser/extensions/webcompat/experiment-apis/trackingProtection.json b/browser/extensions/webcompat/experiment-apis/trackingProtection.json new file mode 100644 index 0000000000..2627d1ea0f --- /dev/null +++ b/browser/extensions/webcompat/experiment-apis/trackingProtection.json @@ -0,0 +1,75 @@ +[ + { + "namespace": "trackingProtection", + "description": "experimental API allow requests through ETP", + "functions": [ + { + "name": "allow", + "type": "function", + "description": "Add specific requests to a given allow-list", + "parameters": [ + { + "name": "allowlistId", + "type": "string" + }, + { + "name": "patterns", + "description": "Array of match patterns", + "type": "array", + "items": { + "type": "string" + } + }, + { + "name": "options", + "type": "object", + "optional": true, + "properties": { + "hosts": { + "description": "Hosts to limit this bypass to (optional)", + "type": "array", + "items": { + "type": "string" + }, + "optional": true + }, + "notHosts": { + "description": "Hosts to not allow this bypass for (optional)", + "type": "array", + "items": { + "type": "string" + }, + "optional": true + } + } + } + ], + "async": true + }, + { + "name": "revoke", + "type": "function", + "description": "Revokes the given allow-list", + "parameters": [ + { + "name": "allowListId", + "type": "string" + } + ], + "async": true + }, + { + "name": "wasRequestUnblocked", + "type": "function", + "description": "Whether the given requestId was unblocked by any allowList", + "parameters": [ + { + "name": "requestId", + "type": "string" + } + ], + "async": true + } + ] + } +] diff --git a/browser/extensions/webcompat/injections/css/bug0000000-testbed-css-injection.css b/browser/extensions/webcompat/injections/css/bug0000000-testbed-css-injection.css new file mode 100644 index 0000000000..1e82ee9722 --- /dev/null +++ b/browser/extensions/webcompat/injections/css/bug0000000-testbed-css-injection.css @@ -0,0 +1,3 @@ +#css-injection.red { + background-color: #0f0; +} diff --git a/browser/extensions/webcompat/injections/css/bug1561371-mail.google.com-allow-horizontal-scrolling.css b/browser/extensions/webcompat/injections/css/bug1561371-mail.google.com-allow-horizontal-scrolling.css new file mode 100644 index 0000000000..15a7fe1484 --- /dev/null +++ b/browser/extensions/webcompat/injections/css/bug1561371-mail.google.com-allow-horizontal-scrolling.css @@ -0,0 +1,12 @@ +/** + * mail.google.com - The HTML email view does not allow horizontal scrolling + * on Firefox mobile due to a missing CSS rule which is only served to Chrome. + * Bug #1561371 - https://bugzilla.mozilla.org/show_bug.cgi?id=1561371 + * + * HTML emails may sometimes contain content that does not wrap, yet the + * CSS served to Firefox Mobile does not permit scrolling horizontally. + * To prevent this UX frustration, we enable horizontal scrolling. + */ +body > #views { + overflow: auto; +} diff --git a/browser/extensions/webcompat/injections/css/bug1570119-teamcoco.com-scrollbar-width.css b/browser/extensions/webcompat/injections/css/bug1570119-teamcoco.com-scrollbar-width.css new file mode 100644 index 0000000000..7a6c2c07c2 --- /dev/null +++ b/browser/extensions/webcompat/injections/css/bug1570119-teamcoco.com-scrollbar-width.css @@ -0,0 +1,11 @@ +/** + * teamcoco.com - a scrollbar at the top covering navigation menu + * Bug #1570119 - https://bugzilla.mozilla.org/show_bug.cgi?id=1570119 + * + * The scrollbar is covering navigation items making them unusable. + * There are ::-webkit-scrollbar css rules already applied to the scrollbar, + * hiding it in Chrome. Adding the scrollbar-width: none fixes the issue in Firefox. + */ +.css-bdnz85 { + scrollbar-width: none; +} diff --git a/browser/extensions/webcompat/injections/css/bug1570328-developer-apple.com-transform-scale.css b/browser/extensions/webcompat/injections/css/bug1570328-developer-apple.com-transform-scale.css new file mode 100644 index 0000000000..2ffd45a361 --- /dev/null +++ b/browser/extensions/webcompat/injections/css/bug1570328-developer-apple.com-transform-scale.css @@ -0,0 +1,17 @@ +/** + * developer.apple.com - content of the page is shifted to the left + * Bug #1570328 - https://bugzilla.mozilla.org/show_bug.cgi?id=1570328 + * WebCompat issue #4070 - https://webcompat.com/issues/4070 + * + * The site is relying on zoom property which is not supported by Mozilla, + * see https://bugzilla.mozilla.org/show_bug.cgi?id=390936. Adding a combination + * of transform: scale(1.4), transform-origin and width fixes the issue + */ +@media only screen and (min-device-width: 320px) and (max-device-width: 980px), + (min-device-width: 1024px) and (max-device-width: 1024px) and (min-device-height: 1366px) and (max-device-height: 1366px) and (min-width: 320px) and (max-width: 980px) { + #tocContainer { + transform-origin: 0 0; + transform: scale(1.4); + width: 71.4%; + } +} diff --git a/browser/extensions/webcompat/injections/css/bug1575000-apply.lloydsbank.co.uk-radio-buttons-fix.css b/browser/extensions/webcompat/injections/css/bug1575000-apply.lloydsbank.co.uk-radio-buttons-fix.css new file mode 100644 index 0000000000..d6c7e82e26 --- /dev/null +++ b/browser/extensions/webcompat/injections/css/bug1575000-apply.lloydsbank.co.uk-radio-buttons-fix.css @@ -0,0 +1,11 @@ +/** + * apply.lloydsbank.co.uk - radio buttons are misplaced + * Bug #1575000 - https://bugzilla.mozilla.org/show_bug.cgi?id=1575000 + * WebCompat issue #34969 - https://webcompat.com/issues/34969 + * + * Radio buttons are displaced to the left due to positioning issue of ::before + * pseudo element, adding position relative to it's parent fixes the issue. + */ +.radio-content-field .radio.inline label span.text { + position: relative; +} diff --git a/browser/extensions/webcompat/injections/css/bug1605611-maps.google.com-directions-time.css b/browser/extensions/webcompat/injections/css/bug1605611-maps.google.com-directions-time.css new file mode 100644 index 0000000000..c0f4fae1f5 --- /dev/null +++ b/browser/extensions/webcompat/injections/css/bug1605611-maps.google.com-directions-time.css @@ -0,0 +1,24 @@ +/** + * Bug 1605611 - Cannot change Departure/arrival dates in Google Maps on Android + * + * This is step 3 - see injections/js/bug1605611-maps.google.com-directions-time.js. + * Google Maps calls .click() on a datetime-local input element, with the intent + * to show the native date picker. But the native date picker does not appear, + * because that only happens when a user initiated the click. + * To fix the problem of the date picker not appearing in Google Maps, alter the + * styles of the datetime-local input element, to be rendered on top of the + * usual UI (i.e. the icon and date/time text). This allows the user to summon + * the native date picker when they tap on the relevant UI in Google Maps. + */ + +.ml-route-options-picker-content-button + > #ml-route-options-time-selector-time-input { + z-index: 1; /* overrides -5000, to show on top of the icon AND the rendered date */ + opacity: 0; /* let the input element be fully transparent */ + width: 100vw; /* render over the rendered date from Maps' dialog */ + /* position this (absolute) element to fully cover the parent container */ + left: 0; + bottom: 0; + top: 0; + height: 100%; +} diff --git a/browser/extensions/webcompat/injections/css/bug1610016-gaana.com-input-position-fix.css b/browser/extensions/webcompat/injections/css/bug1610016-gaana.com-input-position-fix.css new file mode 100644 index 0000000000..258841fdda --- /dev/null +++ b/browser/extensions/webcompat/injections/css/bug1610016-gaana.com-input-position-fix.css @@ -0,0 +1,13 @@ +/** + * gaana.com - unable to accept T&C and Privacy Policy + * Bug #1610016 - https://bugzilla.mozilla.org/show_bug.cgi?id=1610016 + * WebCompat issue #29886 - https://webcompat.com/issues/29886 + * + * Unable to click on checkboxes due to input element floating to the right. + * More info https://bugzilla.mozilla.org/show_bug.cgi?id=997189. Adding explicit + * positioning to the input fixes the issue + */ +.agree_btns input { + top: 0; + left: 0; +} diff --git a/browser/extensions/webcompat/injections/css/bug1610344-directv.com.co-hide-unsupported-message.css b/browser/extensions/webcompat/injections/css/bug1610344-directv.com.co-hide-unsupported-message.css new file mode 100644 index 0000000000..80f94aa306 --- /dev/null +++ b/browser/extensions/webcompat/injections/css/bug1610344-directv.com.co-hide-unsupported-message.css @@ -0,0 +1,13 @@ +/** + * directv.com.co - Browser is not supported message + * Bug #1610344 - https://bugzilla.mozilla.org/show_bug.cgi?id=1610344 + * WebCompat issue #41822 - https://webcompat.com/issues/41822 + * + * directv.com.co is showing a "This browser is not supported" message in + * Firefox. Our tests indicated that everything is working just fine, and our + * previous contact attempts have not been successful. This intervention + * hides the large red unsupported banner. + */ +.browser-compatible.compatible.incompatible { + display: none; +} diff --git a/browser/extensions/webcompat/injections/css/bug1644830-missingmail.usps.com-checkboxes-not-visible.css b/browser/extensions/webcompat/injections/css/bug1644830-missingmail.usps.com-checkboxes-not-visible.css new file mode 100644 index 0000000000..191985b691 --- /dev/null +++ b/browser/extensions/webcompat/injections/css/bug1644830-missingmail.usps.com-checkboxes-not-visible.css @@ -0,0 +1,13 @@ +/** + * missingmail.usps.com - Unable to mark the check-boxes from "Disclaimer and + * Terms and Conditions" section + * Bug #1644830 - https://bugzilla.mozilla.org/show_bug.cgi?id=1644830 + * WebCompat issue #53950 - https://webcompat.com/issues/53950 + * + * missingmail.usps.com runs into a case of bug 997189, where an absolutely + * positioned inline-block element with floating siblings is shifter to the + * right, and thus invisible. + */ +.mrc-custom-checkbox-container input { + margin-left: -3rem; +} diff --git a/browser/extensions/webcompat/injections/css/bug1645064-s-kanava.fi-invisible-charts.css b/browser/extensions/webcompat/injections/css/bug1645064-s-kanava.fi-invisible-charts.css new file mode 100644 index 0000000000..d5c348ad25 --- /dev/null +++ b/browser/extensions/webcompat/injections/css/bug1645064-s-kanava.fi-invisible-charts.css @@ -0,0 +1,12 @@ +/** + * s-kanava.fi - The tables carousel is missing + * Bug #1645064 - https://bugzilla.mozilla.org/show_bug.cgi?id=1645064 + * WebCompat issue #53584 - https://webcompat.com/issues/53584 + * + * This site runs into a known Flex issue, see bug 1469649. However, the issue + * is easy to workaround in this case by explicitly specifying the width of + * the flex container. + */ +.carousel .slider-wrapper.axis-horizontal .slider .slide { + max-width: 100%; +} diff --git a/browser/extensions/webcompat/injections/css/bug1651917-teletrader.com.body-transform-origin.css b/browser/extensions/webcompat/injections/css/bug1651917-teletrader.com.body-transform-origin.css new file mode 100644 index 0000000000..e7a44a93d7 --- /dev/null +++ b/browser/extensions/webcompat/injections/css/bug1651917-teletrader.com.body-transform-origin.css @@ -0,0 +1,14 @@ +/** + * teletrader.com - content is shifted down and right + * Bug #1651917 - https://bugzilla.mozilla.org/show_bug.cgi?id=1651917 + * WebCompat issue #55217 - https://webcompat.com/issues/55217 + * + * The content is shifted down and right, because they use webkit prefixes + * for scaling and redefining the origin. Firefox doesn't support + * -webkit-transform-origin-x/y + * This is the object of https://bugzilla.mozilla.org/show_bug.cgi?id=1584881 + * Adding transform-origin: 0 0; to body fixes the issue + */ +body { + transform-origin: 0 0; +} diff --git a/browser/extensions/webcompat/injections/css/bug1653075-livescience.com-scrollbar-width.css b/browser/extensions/webcompat/injections/css/bug1653075-livescience.com-scrollbar-width.css new file mode 100644 index 0000000000..3d7a069676 --- /dev/null +++ b/browser/extensions/webcompat/injections/css/bug1653075-livescience.com-scrollbar-width.css @@ -0,0 +1,14 @@ +/** + * livescience.com - a scrollbar covering navigation menu + * Bug #1653075 - https://bugzilla.mozilla.org/show_bug.cgi?id=1653075 + * + * The scrollbar is covering navigation items and that makes them half hidden. + * There are some ::-webkit-scrollbar css rules applied to the scrollbar, + * making it thinner. Adding similar rules for Firefox fixes the issue. + */ +@media screen and (max-width: 900px) { + .trending-wrapper .trending-items { + scrollbar-width: thin; + scrollbar-color: #f9ae3b #f5f5f5; + } +} diff --git a/browser/extensions/webcompat/injections/css/bug1654865-sports.ndtv.com-float-fix.css b/browser/extensions/webcompat/injections/css/bug1654865-sports.ndtv.com-float-fix.css new file mode 100644 index 0000000000..f9edefb735 --- /dev/null +++ b/browser/extensions/webcompat/injections/css/bug1654865-sports.ndtv.com-float-fix.css @@ -0,0 +1,13 @@ +/** + * sports.ndtv.com - content is of the articles is not displayed + * Bug #1654865 - https://bugzilla.mozilla.org/show_bug.cgi?id=1654865 + * WebCompat issue #55377 - https://webcompat.com/issues/55377 + * + * The content has width:0, due to uncleared float and negative margin combination, + * which is https://bugzilla.mozilla.org/show_bug.cgi?id=1400958 + * Adding clear: both; to the element located above the affected div + * the fixes the issue + */ +.t-brd { + clear: both; +} diff --git a/browser/extensions/webcompat/injections/css/bug1654877-preev.com-moz-appearance-fix.css b/browser/extensions/webcompat/injections/css/bug1654877-preev.com-moz-appearance-fix.css new file mode 100644 index 0000000000..111ec522df --- /dev/null +++ b/browser/extensions/webcompat/injections/css/bug1654877-preev.com-moz-appearance-fix.css @@ -0,0 +1,15 @@ +/** + * preev.com - typed numbers are not fully visible + * Bug #1654877 - https://bugzilla.mozilla.org/show_bug.cgi?id=1654877 + * WebCompat issue #55099 - https://webcompat.com/issues/55099 + * + * It's hard to see the entered number because the spin button is + * taking too much space. While there is -moz-appearance: textfield, + * -webkit-appearance: none; underneath supersedes it, + * leaving the spin button visible. Adding -moz-appearance: textfield; + * as a separate rule fixes the issue + */ +input[type="number"], +input[type="text"] { + -moz-appearance: textfield; +} diff --git a/browser/extensions/webcompat/injections/css/bug1654907-reactine.ca-hide-unsupported.css b/browser/extensions/webcompat/injections/css/bug1654907-reactine.ca-hide-unsupported.css new file mode 100644 index 0000000000..2893e873ed --- /dev/null +++ b/browser/extensions/webcompat/injections/css/bug1654907-reactine.ca-hide-unsupported.css @@ -0,0 +1,12 @@ +/** + * reactine.ca - Unsupported browser message + * Bug #1654907 - https://bugzilla.mozilla.org/show_bug.cgi?id=1654907 + * WebCompat issue #55481 - https://webcompat.com/issues/55481 + * + * reactine.ca is showing "Sorry this browser is not supported." + * message if Firefox for Android based on UA detection. Site seems + * to be working fine, so this intervention is to hide this message + */ +#browser-alert { + display: none !important; +} diff --git a/browser/extensions/webcompat/injections/css/bug1655049-dev.to-unclickable-button-fix.css b/browser/extensions/webcompat/injections/css/bug1655049-dev.to-unclickable-button-fix.css new file mode 100644 index 0000000000..2e40f14955 --- /dev/null +++ b/browser/extensions/webcompat/injections/css/bug1655049-dev.to-unclickable-button-fix.css @@ -0,0 +1,12 @@ +/** + * dev.to - not possible to open social buttons menu + * Bug #1655049 - https://bugzilla.mozilla.org/show_bug.cgi?id=1655049 + * WebCompat issue #55782 - https://webcompat.com/issues/55782 + * + * Social buttons menu is not opening due to svg receiving the click + * instead of the button. See https://bugzilla.mozilla.org/show_bug.cgi?id=1654934. + * Adding pointer-events: none to the svg allows to open the menu + */ +#article-show-more-button > * { + pointer-events: none; +} diff --git a/browser/extensions/webcompat/injections/css/bug1666771-zilow-map-overdraw.css b/browser/extensions/webcompat/injections/css/bug1666771-zilow-map-overdraw.css new file mode 100644 index 0000000000..382ed99b50 --- /dev/null +++ b/browser/extensions/webcompat/injections/css/bug1666771-zilow-map-overdraw.css @@ -0,0 +1,17 @@ +/** + * zillow.com - Zillow using massive amounts of memory. + * Bug #1666771 - https://bugzilla.mozilla.org/show_bug.cgi?id=1666771 + * Bug #1662297 - https://bugzilla.mozilla.org/show_bug.cgi?id=1662297 + * + * Zillow's map is using a lot of memory, caused by large amounts of overdraw + * inside the map while rendering object boundaries. Setting `overflow: hidden` + * is a workaround until Zillow addressed this in a more permanent way. + * + * Note that this override is not without side effects: some lines in the map + * may/will be cut off. There is no side-effect free solution to this, and + * not intervening means the browser just freezes. + */ + +.zillow-map-layer svg.full-boundary-svg { + overflow: hidden !important; +} diff --git a/browser/extensions/webcompat/injections/js/bug0000000-testbed-js-injection.js b/browser/extensions/webcompat/injections/js/bug0000000-testbed-js-injection.js new file mode 100644 index 0000000000..4e7db8c5f9 --- /dev/null +++ b/browser/extensions/webcompat/injections/js/bug0000000-testbed-js-injection.js @@ -0,0 +1,11 @@ +"use strict"; + +/* globals exportFunction */ + +Object.defineProperty(window.wrappedJSObject, "isTestFeatureSupported", { + get: exportFunction(function() { + return true; + }, window), + + set: exportFunction(function() {}, window), +}); diff --git a/browser/extensions/webcompat/injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js b/browser/extensions/webcompat/injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js new file mode 100644 index 0000000000..d04dcd7638 --- /dev/null +++ b/browser/extensions/webcompat/injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js @@ -0,0 +1,29 @@ +"use strict"; + +/** + * Bug 1452707 - Build site patch for ib.absa.co.za + * WebCompat issue #16401 - https://webcompat.com/issues/16401 + * + * The online banking at ib.absa.co.za detect if window.controllers is a + * non-falsy value to detect if the current browser is Firefox or something + * else. In bug 1448045, this shim has been disabled for Firefox Nightly 61+, + * which breaks the UA detection on this site and results in a "Browser + * unsuppored" error message. + * + * This site patch simply sets window.controllers to a string, resulting in + * their check to work again. + */ + +/* globals exportFunction */ + +console.info( + "window.controllers has been shimmed for compatibility reasons. See https://webcompat.com/issues/16401 for details." +); + +Object.defineProperty(window.wrappedJSObject, "controllers", { + get: exportFunction(function() { + return true; + }, window), + + set: exportFunction(function() {}, window), +}); diff --git a/browser/extensions/webcompat/injections/js/bug1457335-histography.io-ua-change.js b/browser/extensions/webcompat/injections/js/bug1457335-histography.io-ua-change.js new file mode 100644 index 0000000000..8bbab329c4 --- /dev/null +++ b/browser/extensions/webcompat/injections/js/bug1457335-histography.io-ua-change.js @@ -0,0 +1,34 @@ +"use strict"; + +/** + * Bug 1457335 - histography.io - Override UA & navigator.vendor + * WebCompat issue #1804 - https://webcompat.com/issues/1804 + * + * This site is using a strict matching of navigator.userAgent and + * navigator.vendor to allow access for Safari or Chrome. Here, we set the + * values appropriately so we get recognized as Chrome. + */ + +/* globals exportFunction */ + +console.info( + "The user agent has been overridden for compatibility reasons. See https://webcompat.com/issues/1804 for details." +); + +const CHROME_UA = navigator.userAgent + " Chrome for WebCompat"; + +Object.defineProperty(window.navigator.wrappedJSObject, "userAgent", { + get: exportFunction(function() { + return CHROME_UA; + }, window), + + set: exportFunction(function() {}, window), +}); + +Object.defineProperty(window.navigator.wrappedJSObject, "vendor", { + get: exportFunction(function() { + return "Google Inc."; + }, window), + + set: exportFunction(function() {}, window), +}); diff --git a/browser/extensions/webcompat/injections/js/bug1472075-bankofamerica.com-ua-change.js b/browser/extensions/webcompat/injections/js/bug1472075-bankofamerica.com-ua-change.js new file mode 100644 index 0000000000..0f61422494 --- /dev/null +++ b/browser/extensions/webcompat/injections/js/bug1472075-bankofamerica.com-ua-change.js @@ -0,0 +1,48 @@ +"use strict"; + +/** + * Bug 1472075 - Build UA override for Bank of America for OSX & Linux + * WebCompat issue #2787 - https://webcompat.com/issues/2787 + * + * BoA is showing a red warning to Linux and macOS users, while accepting + * Windows users without warning. From our side, there is no difference here + * and we receive a lot of user complains about the warnings, so we spoof + * as Firefox on Windows in those cases. + */ + +/* globals exportFunction */ + +if (!navigator.platform.includes("Win")) { + console.info( + "The user agent has been overridden for compatibility reasons. See https://webcompat.com/issues/2787 for details." + ); + + const WINDOWS_UA = navigator.userAgent.replace( + /\(.*; rv:/i, + "(Windows NT 10.0; Win64; x64; rv:" + ); + + Object.defineProperty(window.navigator.wrappedJSObject, "userAgent", { + get: exportFunction(function() { + return WINDOWS_UA; + }, window), + + set: exportFunction(function() {}, window), + }); + + Object.defineProperty(window.navigator.wrappedJSObject, "appVersion", { + get: exportFunction(function() { + return "appVersion"; + }, window), + + set: exportFunction(function() {}, window), + }); + + Object.defineProperty(window.navigator.wrappedJSObject, "platform", { + get: exportFunction(function() { + return "Win64"; + }, window), + + set: exportFunction(function() {}, window), + }); +} diff --git a/browser/extensions/webcompat/injections/js/bug1570856-medium.com-menu-isTier1.js b/browser/extensions/webcompat/injections/js/bug1570856-medium.com-menu-isTier1.js new file mode 100644 index 0000000000..f8bb926b60 --- /dev/null +++ b/browser/extensions/webcompat/injections/js/bug1570856-medium.com-menu-isTier1.js @@ -0,0 +1,34 @@ +"use strict"; + +/** + * medium.com - Override window.GLOBALS.useragent.isTier1 to be true + * WebCompat issue #25844 - https://webcompat.com/issues/25844 + * + * This site is not showing main menu when scrolling. There is a GLOBALS variable + * at the bottom of the template being defined based on a server side UA detection. + * Setting window.GLOBALS.useragent.isTier1 to true makes the menu appear when scrolling + */ + +/* globals exportFunction */ + +console.info( + "window.GLOBALS.useragent.isTier1 has been set to true for compatibility reasons. See https://webcompat.com/issues/25844 for details." +); + +let globals = {}; + +Object.defineProperty(window.wrappedJSObject, "GLOBALS", { + get: exportFunction(function() { + return globals; + }, window), + + set: exportFunction(function(value = {}) { + globals = value; + + if (!globals.useragent) { + globals.useragent = {}; + } + + globals.useragent.isTier1 = true; + }, window), +}); diff --git a/browser/extensions/webcompat/injections/js/bug1579159-m.tailieu.vn-pdfjs-worker-disable.js b/browser/extensions/webcompat/injections/js/bug1579159-m.tailieu.vn-pdfjs-worker-disable.js new file mode 100644 index 0000000000..b6600e93f8 --- /dev/null +++ b/browser/extensions/webcompat/injections/js/bug1579159-m.tailieu.vn-pdfjs-worker-disable.js @@ -0,0 +1,28 @@ +"use strict"; + +/** + * m.tailieu.vn - Override PDFJS.disableWorker to be true + * WebCompat issue #39057 - https://webcompat.com/issues/39057 + * + * Custom viewer built with PDF.js is not working in Firefox for Android + * Disabling worker to match Chrome behavior fixes the issue + */ + +/* globals exportFunction */ + +console.info( + "window.PDFJS.disableWorker has been set to true for compatibility reasons. See https://webcompat.com/issues/39057 for details." +); + +let globals = {}; + +Object.defineProperty(window.wrappedJSObject, "PDFJS", { + get: exportFunction(function() { + return globals; + }, window), + + set: exportFunction(function(value = {}) { + globals = value; + globals.disableWorker = true; + }, window), +}); diff --git a/browser/extensions/webcompat/injections/js/bug1605611-maps.google.com-directions-time.js b/browser/extensions/webcompat/injections/js/bug1605611-maps.google.com-directions-time.js new file mode 100644 index 0000000000..aee07df0cc --- /dev/null +++ b/browser/extensions/webcompat/injections/js/bug1605611-maps.google.com-directions-time.js @@ -0,0 +1,82 @@ +"use strict"; + +/* globals exportFunction */ + +/** + * Bug 1605611 - Cannot change Departure/arrival dates in Google Maps on Android + * + * This patch does the following: + * 1. Re-enable the disabled "Leave now" button. + * 2. Fix the precision of datetime-local inputs (to minutes). + * 3. Fixup side effect from enabling the date picker UI via + * injections/css/bug1605611-maps.google.com-directions-time.css + * + * See https://bugzilla.mozilla.org/show_bug.cgi?id=1605611#c0 for details. + */ + +// Step 1. +document.addEventListener("DOMContentLoaded", () => { + // In case the element appeared before the MutationObserver was activated. + for (const elem of document.querySelectorAll( + ".ml-directions-time[disabled]" + )) { + elem.disabled = false; + } + // Start watching for the insertion of the "Leave now" button. + const moOptions = { + attributeFilter: ["disabled"], + attributes: true, + subtree: true, + }; + const mo = new MutationObserver(function(records) { + let restore = false; + for (const { target } of records) { + if (target.classList.contains("ml-directions-time")) { + if (!restore) { + restore = true; + mo.disconnect(); + } + target.disabled = false; + } + } + if (restore) { + mo.observe(document.body, moOptions); + } + }); + mo.observe(document.body, moOptions); +}); + +// Step 2. +const originalValueAsNumberGetter = Object.getOwnPropertyDescriptor( + HTMLInputElement.prototype.wrappedJSObject, + "valueAsNumber" +).get; +Object.defineProperty( + HTMLInputElement.prototype.wrappedJSObject, + "valueAsNumber", + { + configurable: true, + enumerable: true, + get: originalValueAsNumberGetter, + set: exportFunction(function(v) { + if (this.type === "datetime-local" && v) { + const d = new Date(v); + d.setSeconds(0); + d.setMilliseconds(0); + v = d.getTime(); + } + this.valueAsNumber = v; + }, window), + } +); + +// Step 3. +// injections/css/bug1605611-maps.google.com-directions-time.css fixes the bug, +// but a side effect of allowing the user to click on the datetime-local input +// is that the keyboard appears when the native date picker is closed. +// Fix this by unfocusing the datetime-local input upon focus. +document.addEventListener("focusin", ({ target }) => { + if (target.id === "ml-route-options-time-selector-time-input") { + target.blur(); + } +}); diff --git a/browser/extensions/webcompat/injections/js/bug1610358-pcloud.com-appVersion-change.js b/browser/extensions/webcompat/injections/js/bug1610358-pcloud.com-appVersion-change.js new file mode 100644 index 0000000000..c7654227e1 --- /dev/null +++ b/browser/extensions/webcompat/injections/js/bug1610358-pcloud.com-appVersion-change.js @@ -0,0 +1,25 @@ +"use strict"; + +/** + * Bug 1610358 - Add "mobile" to navigator.appVersion + * WebCompat issue #40353 - https://webcompat.com/issues/40353 + * + * the site expecting navigator.appVersion to contain "mobile", + * otherwise it's serving a tablet version for Firefox mobile + */ + +/* globals exportFunction */ + +console.info( + "The user agent has been overridden for compatibility reasons. See https://webcompat.com/issues/40353 for details." +); + +const APP_VERSION = navigator.appVersion + " mobile"; + +Object.defineProperty(window.navigator.wrappedJSObject, "appVersion", { + get: exportFunction(function() { + return APP_VERSION; + }, window), + + set: exportFunction(function() {}, window), +}); diff --git a/browser/extensions/webcompat/injections/js/bug1631811-datastudio.google.com-indexedDB.js b/browser/extensions/webcompat/injections/js/bug1631811-datastudio.google.com-indexedDB.js new file mode 100644 index 0000000000..63bb420d8d --- /dev/null +++ b/browser/extensions/webcompat/injections/js/bug1631811-datastudio.google.com-indexedDB.js @@ -0,0 +1,18 @@ +"use strict"; + +/** + * Bug 1631811 - disable indexedDB for datastudio.google.com iframes + * + * Indexed DB is disabled already for these iframes due to cookie blocking. + * This intervention changes the functionality from throwing a SecurityError + * when indexedDB is accessed to removing it from the window object + */ + +console.info( + "window.indexedDB has been overwritten for compatibility reasons. See https://bugzilla.mozilla.org/show_bug.cgi?id=1631811 for details." +); + +Object.defineProperty(window.wrappedJSObject, "indexedDB", { + get: undefined, + set: undefined, +}); diff --git a/browser/extensions/webcompat/injections/js/bug1665035-dckids.com-cookieEnabled.js b/browser/extensions/webcompat/injections/js/bug1665035-dckids.com-cookieEnabled.js new file mode 100644 index 0000000000..a243fae54a --- /dev/null +++ b/browser/extensions/webcompat/injections/js/bug1665035-dckids.com-cookieEnabled.js @@ -0,0 +1,30 @@ +"use strict"; + +/** + * Bug 1665035 - enable navigator.cookieEnabled and spoof window.navigator on Linux + * + * Some of the games are not starting because navigator.cookieEnabled + * returns false for trackers with ETP strict. Overwriting the value allows + * to play the games. In addition, Linux desktop devices are incorrectly + * flagged as mobile devices (even if ETP is disabled), so spoofing + * window.navigator.platform here. + */ + +console.info( + "window.cookieEnabled has been overwritten for compatibility reasons. See https://bugzilla.mozilla.org/show_bug.cgi?id=1665035 for details." +); + +Object.defineProperty(window.navigator.wrappedJSObject, "cookieEnabled", { + value: true, + writable: false, +}); + +if (navigator.platform.includes("Linux")) { + console.info( + "navigator.platform has been overwritten for compatibility reasons. See https://bugzilla.mozilla.org/show_bug.cgi?id=1665035 for details." + ); + Object.defineProperty(window.navigator.wrappedJSObject, "platform", { + value: "Win64", + writable: false, + }); +} diff --git a/browser/extensions/webcompat/injections/js/bug1677442-store.hp.com-disable-indexeddb.js b/browser/extensions/webcompat/injections/js/bug1677442-store.hp.com-disable-indexeddb.js new file mode 100644 index 0000000000..507e39cff5 --- /dev/null +++ b/browser/extensions/webcompat/injections/js/bug1677442-store.hp.com-disable-indexeddb.js @@ -0,0 +1,20 @@ +"use strict"; + +/** + * Bug 1677442 - disable indexedDB for d3nkfb7815bs43.cloudfront.net + * + * The site embeds an iframe with a 3D viewer. The request fails + * because BabylonJS (the 3d library) tries to access indexedDB + * from the third party context (d3nkfb7815bs43.cloudfront.net) + * Disabling indexedDB fixes it, causing it to fetch the 3d resource + * via network. + */ + +console.info( + "window.indexedDB has been overwritten for compatibility reasons. See https://bugzilla.mozilla.org/show_bug.cgi?id=1677442 for details." +); + +Object.defineProperty(window.wrappedJSObject, "indexedDB", { + get: undefined, + set: undefined, +}); diff --git a/browser/extensions/webcompat/injections/js/bug1682238-gamearter.com-ua-change.js b/browser/extensions/webcompat/injections/js/bug1682238-gamearter.com-ua-change.js new file mode 100644 index 0000000000..8475a9486f --- /dev/null +++ b/browser/extensions/webcompat/injections/js/bug1682238-gamearter.com-ua-change.js @@ -0,0 +1,27 @@ +"use strict"; + +/* + * Bug 1682238 - Override navigator.userAgent for gamearter.com on macOS 11.0 + * Bug 1680516 - Game is not loaded on gamearter.com + * + * Unity < 2021.1.0a2 is unable to correctly parse User Agents with + * "Mac OS X 11.0" in them, so let's override to "Mac OS X 10.16" instead + * for now. + */ + +/* globals exportFunction */ + +if (navigator.userAgent.includes("Mac OS X 11.")) { + console.info( + "The user agent has been overridden for compatibility reasons. See https://bugzilla.mozilla.org/show_bug.cgi?id=1680516 for details." + ); + + let originalUA = navigator.userAgent; + Object.defineProperty(window.navigator.wrappedJSObject, "userAgent", { + get: exportFunction(function() { + return originalUA.replace(/Mac OS X 11\.(\d)+;/, "Mac OS X 10.16;"); + }, window), + + set: exportFunction(function() {}, window), + }); +} diff --git a/browser/extensions/webcompat/lib/about_compat_broker.js b/browser/extensions/webcompat/lib/about_compat_broker.js new file mode 100644 index 0000000000..dc939b2b06 --- /dev/null +++ b/browser/extensions/webcompat/lib/about_compat_broker.js @@ -0,0 +1,123 @@ +/* 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"; + +/* global browser, module, onMessageFromTab */ + +class AboutCompatBroker { + constructor(bindings) { + this.portsToAboutCompatTabs = this.buildPorts(); + + this._injections = bindings.injections; + this._injections.bindAboutCompatBroker(this); + + this._uaOverrides = bindings.uaOverrides; + this._uaOverrides.bindAboutCompatBroker(this); + } + + buildPorts() { + const ports = new Set(); + + browser.runtime.onConnect.addListener(port => { + ports.add(port); + port.onDisconnect.addListener(function() { + ports.delete(port); + }); + }); + + async function broadcast(message) { + for (const port of ports) { + port.postMessage(message); + } + } + + return { broadcast }; + } + + filterOverrides(overrides) { + return overrides + .filter(override => override.availableOnPlatform) + .map(override => { + const { id, active, bug, domain, hidden } = override; + return { id, active, bug, domain, hidden }; + }); + } + + getOverrideOrInterventionById(id) { + for (const [type, things] of Object.entries({ + overrides: this._uaOverrides.getAvailableOverrides(), + interventions: this._injections.getAvailableInjections(), + })) { + for (const what of things) { + if (what.id === id) { + return { type, what }; + } + } + } + return {}; + } + + bootup() { + onMessageFromTab(msg => { + switch (msg.command || msg) { + case "toggle": { + const id = msg.id; + const { type, what } = this.getOverrideOrInterventionById(id); + if (!what) { + return Promise.reject( + `No such override or intervention to toggle: ${id}` + ); + } + this.portsToAboutCompatTabs + .broadcast({ toggling: id, active: what.active }) + .then(async () => { + switch (type) { + case "interventions": { + if (what.active) { + await this._injections.disableInjection(what); + } else { + await this._injections.enableInjection(what); + } + break; + } + case "overrides": { + if (what.active) { + await this._uaOverrides.disableOverride(what); + } else { + await this._uaOverrides.enableOverride(what); + } + break; + } + } + this.portsToAboutCompatTabs.broadcast({ + toggled: id, + active: what.active, + }); + }); + break; + } + case "getOverridesAndInterventions": { + return Promise.resolve({ + overrides: + (this._uaOverrides.isEnabled() && + this.filterOverrides( + this._uaOverrides.getAvailableOverrides() + )) || + false, + interventions: + (this._injections.isEnabled() && + this.filterOverrides( + this._injections.getAvailableInjections() + )) || + false, + }); + } + } + return undefined; + }); + } +} + +module.exports = AboutCompatBroker; diff --git a/browser/extensions/webcompat/lib/custom_functions.js b/browser/extensions/webcompat/lib/custom_functions.js new file mode 100644 index 0000000000..9bfc6fbdb5 --- /dev/null +++ b/browser/extensions/webcompat/lib/custom_functions.js @@ -0,0 +1,96 @@ +/* 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 */ + +const replaceStringInRequest = ( + requestId, + inString, + outString, + inEncoding = "utf-8" +) => { + const filter = browser.webRequest.filterResponseData(requestId); + const decoder = new TextDecoder(inEncoding); + const encoder = new TextEncoder(); + const RE = new RegExp(inString, "g"); + const carryoverLength = inString.length; + let carryover = ""; + + filter.ondata = event => { + const replaced = ( + carryover + decoder.decode(event.data, { stream: true }) + ).replace(RE, outString); + filter.write(encoder.encode(replaced.slice(0, -carryoverLength))); + carryover = replaced.slice(-carryoverLength); + }; + + filter.onstop = event => { + if (carryover.length) { + filter.write(encoder.encode(carryover)); + } + filter.close(); + }; +}; + +const CUSTOM_FUNCTIONS = { + detectSwipeFix: injection => { + const { urls, types } = injection.data; + const listener = (injection.data.listener = ({ requestId }) => { + replaceStringInRequest( + requestId, + "preventDefault:true", + "preventDefault:false" + ); + return {}; + }); + browser.webRequest.onBeforeRequest.addListener(listener, { urls, types }, [ + "blocking", + ]); + }, + detectSwipeFixDisable: injection => { + const { listener } = injection.data; + browser.webRequest.onBeforeRequest.removeListener(listener); + delete injection.data.listener; + }, + noSniffFix: injection => { + const { urls, contentType } = injection.data; + const listener = (injection.data.listener = e => { + e.responseHeaders.push(contentType); + return { responseHeaders: e.responseHeaders }; + }); + + browser.webRequest.onHeadersReceived.addListener(listener, { urls }, [ + "blocking", + "responseHeaders", + ]); + }, + noSniffFixDisable: injection => { + const { listener } = injection.data; + browser.webRequest.onHeadersReceived.removeListener(listener); + delete injection.data.listener; + }, + pdk5fix: injection => { + const { urls, types } = injection.data; + const listener = (injection.data.listener = ({ requestId }) => { + replaceStringInRequest( + requestId, + "VideoContextChromeAndroid", + "VideoContextAndroid" + ); + return {}; + }); + browser.webRequest.onBeforeRequest.addListener(listener, { urls, types }, [ + "blocking", + ]); + }, + pdk5fixDisable: injection => { + const { listener } = injection.data; + browser.webRequest.onBeforeRequest.removeListener(listener); + delete injection.data.listener; + }, +}; + +module.exports = CUSTOM_FUNCTIONS; diff --git a/browser/extensions/webcompat/lib/injections.js b/browser/extensions/webcompat/lib/injections.js new file mode 100644 index 0000000000..6a7a6e8d4a --- /dev/null +++ b/browser/extensions/webcompat/lib/injections.js @@ -0,0 +1,163 @@ +/* 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 */ + +class Injections { + constructor(availableInjections, customFunctions) { + this.INJECTION_PREF = "perform_injections"; + + this._injectionsEnabled = true; + + this._availableInjections = availableInjections; + this._activeInjections = new Map(); + this._customFunctions = customFunctions; + } + + bindAboutCompatBroker(broker) { + this._aboutCompatBroker = broker; + } + + bootup() { + browser.aboutConfigPrefs.onPrefChange.addListener(() => { + this.checkInjectionPref(); + }, this.INJECTION_PREF); + this.checkInjectionPref(); + } + + checkInjectionPref() { + browser.aboutConfigPrefs.getPref(this.INJECTION_PREF).then(value => { + if (value === undefined) { + browser.aboutConfigPrefs.setPref(this.INJECTION_PREF, true); + } else if (value === false) { + this.unregisterContentScripts(); + } else { + this.registerContentScripts(); + } + }); + } + + getAvailableInjections() { + return this._availableInjections; + } + + isEnabled() { + return this._injectionsEnabled; + } + + async registerContentScripts() { + const platformMatches = ["all"]; + let platformInfo = await browser.runtime.getPlatformInfo(); + platformMatches.push(platformInfo.os == "android" ? "android" : "desktop"); + + for (const injection of this._availableInjections) { + if (platformMatches.includes(injection.platform)) { + injection.availableOnPlatform = true; + await this.enableInjection(injection); + } + } + + this._injectionsEnabled = true; + this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({ + interventionsChanged: this._aboutCompatBroker.filterOverrides( + this._availableInjections + ), + }); + } + + assignContentScriptDefaults(contentScripts) { + let finalConfig = Object.assign({}, contentScripts); + + if (!finalConfig.runAt) { + finalConfig.runAt = "document_start"; + } + + return finalConfig; + } + + async enableInjection(injection) { + if (injection.active) { + return undefined; + } + + if (injection.customFunc) { + return this.enableCustomInjection(injection); + } + + return this.enableContentScripts(injection); + } + + enableCustomInjection(injection) { + if (injection.customFunc in this._customFunctions) { + this._customFunctions[injection.customFunc](injection); + injection.active = true; + } else { + console.error( + `Provided function ${injection.customFunc} wasn't found in functions list` + ); + } + } + + async enableContentScripts(injection) { + try { + const handle = await browser.contentScripts.register( + this.assignContentScriptDefaults(injection.contentScripts) + ); + this._activeInjections.set(injection, handle); + injection.active = true; + } catch (ex) { + console.error( + "Registering WebCompat GoFaster content scripts failed: ", + ex + ); + } + } + + unregisterContentScripts() { + for (const injection of this._availableInjections) { + this.disableInjection(injection); + } + + this._injectionsEnabled = false; + this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({ + interventionsChanged: false, + }); + } + + async disableInjection(injection) { + if (!injection.active) { + return undefined; + } + + if (injection.customFunc) { + return this.disableCustomInjections(injection); + } + + return this.disableContentScripts(injection); + } + + disableCustomInjections(injection) { + const disableFunc = injection.customFunc + "Disable"; + + if (disableFunc in this._customFunctions) { + this._customFunctions[disableFunc](injection); + injection.active = false; + } else { + console.error( + `Provided function ${disableFunc} for disabling injection wasn't found in functions list` + ); + } + } + + async disableContentScripts(injection) { + const contentScript = this._activeInjections.get(injection); + await contentScript.unregister(); + this._activeInjections.delete(injection); + injection.active = false; + } +} + +module.exports = Injections; diff --git a/browser/extensions/webcompat/lib/intervention_helpers.js b/browser/extensions/webcompat/lib/intervention_helpers.js new file mode 100644 index 0000000000..16ea6572f2 --- /dev/null +++ b/browser/extensions/webcompat/lib/intervention_helpers.js @@ -0,0 +1,233 @@ +/* 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 module */ + +const GOOGLE_TLDS = [ + "com", + "ac", + "ad", + "ae", + "com.af", + "com.ag", + "com.ai", + "al", + "am", + "co.ao", + "com.ar", + "as", + "at", + "com.au", + "az", + "ba", + "com.bd", + "be", + "bf", + "bg", + "com.bh", + "bi", + "bj", + "com.bn", + "com.bo", + "com.br", + "bs", + "bt", + "co.bw", + "by", + "com.bz", + "ca", + "com.kh", + "cc", + "cd", + "cf", + "cat", + "cg", + "ch", + "ci", + "co.ck", + "cl", + "cm", + "cn", + "com.co", + "co.cr", + "com.cu", + "cv", + "com.cy", + "cz", + "de", + "dj", + "dk", + "dm", + "com.do", + "dz", + "com.ec", + "ee", + "com.eg", + "es", + "com.et", + "fi", + "com.fj", + "fm", + "fr", + "ga", + "ge", + "gf", + "gg", + "com.gh", + "com.gi", + "gl", + "gm", + "gp", + "gr", + "com.gt", + "gy", + "com.hk", + "hn", + "hr", + "ht", + "hu", + "co.id", + "iq", + "ie", + "co.il", + "im", + "co.in", + "io", + "is", + "it", + "je", + "com.jm", + "jo", + "co.jp", + "co.ke", + "ki", + "kg", + "co.kr", + "com.kw", + "kz", + "la", + "com.lb", + "com.lc", + "li", + "lk", + "co.ls", + "lt", + "lu", + "lv", + "com.ly", + "co.ma", + "md", + "me", + "mg", + "mk", + "ml", + "com.mm", + "mn", + "ms", + "com.mt", + "mu", + "mv", + "mw", + "com.mx", + "com.my", + "co.mz", + "com.na", + "ne", + "com.nf", + "com.ng", + "com.ni", + "nl", + "no", + "com.np", + "nr", + "nu", + "co.nz", + "com.om", + "com.pk", + "com.pa", + "com.pe", + "com.ph", + "pl", + "com.pg", + "pn", + "com.pr", + "ps", + "pt", + "com.py", + "com.qa", + "ro", + "rs", + "ru", + "rw", + "com.sa", + "com.sb", + "sc", + "se", + "com.sg", + "sh", + "si", + "sk", + "com.sl", + "sn", + "sm", + "so", + "st", + "sr", + "com.sv", + "td", + "tg", + "co.th", + "com.tj", + "tk", + "tl", + "tm", + "to", + "tn", + "com.tr", + "tt", + "com.tw", + "co.tz", + "com.ua", + "co.ug", + "co.uk", + "com", + "com.uy", + "co.uz", + "com.vc", + "co.ve", + "vg", + "co.vi", + "com.vn", + "vu", + "ws", + "co.za", + "co.zm", + "co.zw", +]; + +var InterventionHelpers = { + /** + * Useful helper to generate a list of domains with a fixed base domain and + * multiple country-TLDs or other cases with various TLDs. + * + * Example: + * matchPatternsForTLDs("*://mozilla.", "/*", ["com", "org"]) + * => ["*://mozilla.com/*", "*://mozilla.org/*"] + */ + matchPatternsForTLDs(base, suffix, tlds) { + return tlds.map(tld => base + tld + suffix); + }, + + /** + * A modified version of matchPatternsForTLDs that always returns the match + * list for all known Google country TLDs. + */ + matchPatternsForGoogle(base, suffix = "/*") { + return InterventionHelpers.matchPatternsForTLDs(base, suffix, GOOGLE_TLDS); + }, +}; + +module.exports = InterventionHelpers; diff --git a/browser/extensions/webcompat/lib/messaging_helper.js b/browser/extensions/webcompat/lib/messaging_helper.js new file mode 100644 index 0000000000..793fa03139 --- /dev/null +++ b/browser/extensions/webcompat/lib/messaging_helper.js @@ -0,0 +1,36 @@ +/* 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 */ + +// By default, only the first handler for browser.runtime.onMessage which +// returns a value will get to return one. As such, we need to let them all +// receive the message, and all have a chance to return a response (with the +// first non-undefined result being the one that is ultimately returned). +// This way, about:compat and the shims library can both get a chance to +// process a message, and just return undefined if they wish to ignore it. + +const onMessageFromTab = (function() { + const handlers = new Set(); + + browser.runtime.onMessage.addListener((msg, sender) => { + const promises = [...handlers.values()].map(fn => fn(msg, sender)); + return Promise.allSettled(promises).then(results => { + for (const { reason, value } of results) { + if (reason) { + console.error(reason); + } else if (value !== undefined) { + return value; + } + } + return undefined; + }); + }); + + return function(handler) { + handlers.add(handler); + }; +})(); diff --git a/browser/extensions/webcompat/lib/module_shim.js b/browser/extensions/webcompat/lib/module_shim.js new file mode 100644 index 0000000000..2fd39fdbbd --- /dev/null +++ b/browser/extensions/webcompat/lib/module_shim.js @@ -0,0 +1,24 @@ +/* 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"; + +/** + * We cannot yet use proper JS modules within webextensions, as support for them + * is highly experimental and highly instable. So we end up just including all + * the JS files we need as separate background scripts, and since they all are + * executed within the same context, this works for our in-browser deployment. + * + * However, this code is tracked outside of mozilla-central, and we work on + * shipping this code in other products, like android-components as well. + * Because of that, we have automated tests running within that repository. To + * make our lives easier, we add `module.exports` statements to the JS source + * files, so we can easily import their contents into our NodeJS-based test + * suite. + * + * This works fine, but obviously, `module` is not defined when running + * in-browser. So let's use this empty object as a shim, so we don't run into + * runtime exceptions because of that. + */ +var module = {}; diff --git a/browser/extensions/webcompat/lib/picture_in_picture_overrides.js b/browser/extensions/webcompat/lib/picture_in_picture_overrides.js new file mode 100644 index 0000000000..febee193aa --- /dev/null +++ b/browser/extensions/webcompat/lib/picture_in_picture_overrides.js @@ -0,0 +1,74 @@ +/* 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 */ + +class PictureInPictureOverrides { + constructor(availableOverrides) { + this.pref = "enable_picture_in_picture_overrides"; + this._prefEnabledOverrides = new Set(); + this._availableOverrides = availableOverrides; + this.policies = browser.pictureInPictureChild.getPolicies(); + } + + async _checkGlobalPref() { + await browser.aboutConfigPrefs.getPref(this.pref).then(value => { + if (value === false) { + this._enabled = false; + } else { + if (value === undefined) { + browser.aboutConfigPrefs.setPref(this.pref, true); + } + this._enabled = true; + } + }); + } + + async _checkSpecificOverridePref(id, pref) { + const isDisabled = await browser.aboutConfigPrefs.getPref(pref); + if (isDisabled === true) { + this._prefEnabledOverrides.delete(id); + } else { + this._prefEnabledOverrides.add(id); + } + } + + bootup() { + const checkGlobal = async () => { + await this._checkGlobalPref(); + this._onAvailableOverridesChanged(); + }; + browser.aboutConfigPrefs.onPrefChange.addListener(checkGlobal, this.pref); + + const bootupPrefCheckPromises = [this._checkGlobalPref()]; + + for (const id of Object.keys(this._availableOverrides)) { + const pref = `disabled_picture_in_picture_overrides.${id}`; + const checkSingle = async () => { + await this._checkSpecificOverridePref(id, pref); + this._onAvailableOverridesChanged(); + }; + browser.aboutConfigPrefs.onPrefChange.addListener(checkSingle, pref); + bootupPrefCheckPromises.push(this._checkSpecificOverridePref(id, pref)); + } + + Promise.all(bootupPrefCheckPromises).then(() => { + this._onAvailableOverridesChanged(); + }); + } + + async _onAvailableOverridesChanged() { + const policies = await this.policies; + let enabledOverrides = {}; + for (const [id, override] of Object.entries(this._availableOverrides)) { + const enabled = this._enabled && this._prefEnabledOverrides.has(id); + for (const [url, policy] of Object.entries(override)) { + enabledOverrides[url] = enabled ? policy : policies.DEFAULT; + } + } + browser.pictureInPictureParent.setOverrides(enabledOverrides); + } +} diff --git a/browser/extensions/webcompat/lib/shim_messaging_helper.js b/browser/extensions/webcompat/lib/shim_messaging_helper.js new file mode 100644 index 0000000000..ee109713a5 --- /dev/null +++ b/browser/extensions/webcompat/lib/shim_messaging_helper.js @@ -0,0 +1,65 @@ +/* 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 */ + +if (!window.Shims) { + window.Shims = new Map(); +} + +if (!window.ShimsHelperReady) { + window.ShimsHelperReady = true; + + browser.runtime.onMessage.addListener(details => { + const { shimId, warning } = details; + if (!shimId) { + return; + } + window.Shims.set(shimId, details); + if (warning) { + console.warn(warning); + } + }); + + async function handleMessage(port, shimId, messageId, message) { + let response; + const shim = window.Shims.get(shimId); + if (shim) { + const { needsShimHelpers, origin } = shim; + if (origin === location.origin) { + if (needsShimHelpers?.includes(message)) { + const msg = { shimId, message }; + try { + response = await browser.runtime.sendMessage(msg); + } catch (_) {} + } + } + } + port.postMessage({ messageId, response }); + } + + window.addEventListener( + "ShimConnects", + e => { + e.stopPropagation(); + e.preventDefault(); + const { port, pendingMessages, shimId } = e.detail; + const shim = window.Shims.get(shimId); + if (!shim) { + return; + } + port.onmessage = ({ data }) => { + handleMessage(port, shimId, data.messageId, data.message); + }; + for (const [messageId, message] of pendingMessages) { + handleMessage(port, shimId, messageId, message); + } + }, + true + ); + + window.dispatchEvent(new CustomEvent("ShimHelperReady")); +} diff --git a/browser/extensions/webcompat/lib/shims.js b/browser/extensions/webcompat/lib/shims.js new file mode 100644 index 0000000000..638411007b --- /dev/null +++ b/browser/extensions/webcompat/lib/shims.js @@ -0,0 +1,415 @@ +/* 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 */ + +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) !== "beta_or_release") { + console.debug.apply(this, arguments); + } +}; +let error = async function() { + if ((await releaseBranchPromise) !== "beta_or_release") { + console.error.apply(this, arguments); + } +}; +let warn = async function() { + if ((await releaseBranchPromise) !== "beta_or_release") { + console.warn.apply(this, arguments); + } +}; + +class Shim { + constructor(opts) { + const { matches, unblocksOnOptIn } = opts; + + this.branches = opts.branches; + this.bug = opts.bug; + this.file = opts.file; + this.hosts = opts.hosts; + this.id = opts.id; + this.matches = matches; + this.name = opts.name; + this.notHosts = opts.notHosts; + this.onlyIfBlockedByETP = opts.onlyIfBlockedByETP; + this._options = opts.options || {}; + this.needsShimHelpers = opts.needsShimHelpers; + this.platform = opts.platform || "all"; + this.unblocksOnOptIn = unblocksOnOptIn; + + this._hostOptIns = new Set(); + + this._disabledByConfig = opts.disabled; + this._disabledGlobally = false; + this._disabledByPlatform = false; + this._disabledByReleaseBranch = false; + + const pref = `disabled_shims.${this.id}`; + + 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).then(value => { + this._disabledPrefValue = value; + }), + platformPromise.then(platform => { + this._disabledByPlatform = + this.platform !== "all" && this.platform !== platform; + return platform; + }), + releaseBranchPromise.then(branch => { + this._disabledByReleaseBranch = + this.branches && !this.branches.includes(branch); + return branch; + }), + ]).then(([_, platform, branch]) => { + 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) { + return false; + } + + if (this._disabledPrefValue !== undefined) { + return !this._disabledPrefValue; + } + + return ( + !this._disabledByConfig && + !this._disabledByPlatform && + !this._disabledByReleaseBranch + ); + } + + enable() { + this._disabledGlobally = false; + this._onEnabledStateChanged(); + } + + disable() { + this._disabledGlobally = true; + this._onEnabledStateChanged(); + } + + _onEnabledStateChanged() { + if (!this.enabled) { + return this._revokeRequestsInETP(); + } + return this._allowRequestsInETP(); + } + + _allowRequestsInETP() { + return browser.trackingProtection.allow(this.id, this.matches, { + hosts: this.hosts, + notHosts: this.notHosts, + }); + } + + _revokeRequestsInETP() { + return browser.trackingProtection.revoke(this.id); + } + + meantForHost(host) { + const { hosts, notHosts } = this; + if (hosts || notHosts) { + if ( + (notHosts && notHosts.includes(host)) || + (hosts && !hosts.includes(host)) + ) { + return false; + } + } + return true; + } + + isTriggeredByURL(url) { + if (!this.matches) { + return false; + } + + if (!this._matcher) { + this._matcher = browser.matchPatterns.getMatcher(this.matches); + } + + return this._matcher.matches(url); + } + + async onUserOptIn(host) { + const { unblocksOnOptIn } = this; + if (unblocksOnOptIn) { + await browser.trackingProtection.allow(this.id, unblocksOnOptIn, { + hosts: [host], + }); + } + + this._hostOptIns.add(host); + } + + hasUserOptedInAlready(host) { + return this._hostOptIns.has(host); + } +} + +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(); + } + + _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)); + } + } + + const allShimPatterns = new Set(); + for (const { matches } of this.shims.values()) { + for (const matchPattern of matches) { + allShimPatterns.add(matchPattern); + } + } + + if (!allShimPatterns.size) { + debug("Skipping shims; none enabled"); + return; + } + + const urls = [...allShimPatterns]; + debug("Shimming these match patterns", urls); + + browser.webRequest.onBeforeRequest.addListener( + this._ensureShimForRequestOnTab.bind(this), + { urls, types: ["script"] }, + ["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.enable(); + } else { + shim.disable(); + } + } + } + + async _onMessageFromShim(payload, sender, sendResponse) { + const { tab } = 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 shim.options; + } else if (message === "optIn") { + try { + await shim.onUserOptIn(new URL(url).hostname); + warn("** User opted in on tab ", id, "for", shimId); + } catch (err) { + console.error(err); + throw new Error("error"); + } + } + + return undefined; + } + + 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, 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 shimToApply; + for (const shim of this.shims.values()) { + await shim.ready; + + if (!shim.enabled) { + 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 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)) { + return undefined; + } + + // If this URL isn't meant for this shim, don't apply it. + if (!shim.isTriggeredByURL(url)) { + continue; + } + + shimToApply = shim; + break; + } + + 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 re-run the + // shim JS just in case. Shims should gracefully handle this as well. + const { bug, file, id, name, needsShimHelpers } = shimToApply; + warn("Shimming", name, "on tabId", tabId, "frameId", frameId); + + const warning = `${name} is being shimmed by Firefox. See https://bugzilla.mozilla.org/show_bug.cgi?id=${bug} for details.`; + + try { + if (needsShimHelpers?.length) { + 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 } + ); + } else { + await browser.tabs.executeScript(tabId, { + code: `console.warn(${JSON.stringify(warning)})`, + frameId, + runAt: "document_start", + }); + } + } catch (_) {} + + // If any shims matched the script to replace it, then let the original + // request complete without ever hitting the network, with a blank script. + return { redirectUrl: browser.runtime.getURL(`shims/${file}`) }; + } + + // Sanity check: if no shims are over-riding a given URL and it was meant to + // be blocked by ETP, then block it. + if (unblocked) { + error("unexpected:", url, "was not shimmed, and had to be re-blocked"); + return { cancel: true }; + } + + debug("allowing", url); + return undefined; + } +} + +module.exports = Shims; diff --git a/browser/extensions/webcompat/lib/ua_overrides.js b/browser/extensions/webcompat/lib/ua_overrides.js new file mode 100644 index 0000000000..7024583e4e --- /dev/null +++ b/browser/extensions/webcompat/lib/ua_overrides.js @@ -0,0 +1,265 @@ +/* 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 */ + +class UAOverrides { + constructor(availableOverrides) { + this.OVERRIDE_PREF = "perform_ua_overrides"; + + this._overridesEnabled = true; + + this._availableOverrides = availableOverrides; + this._activeListeners = new Map(); + } + + bindAboutCompatBroker(broker) { + this._aboutCompatBroker = broker; + } + + bootup() { + browser.aboutConfigPrefs.onPrefChange.addListener(() => { + this.checkOverridePref(); + }, this.OVERRIDE_PREF); + this.checkOverridePref(); + } + + checkOverridePref() { + browser.aboutConfigPrefs.getPref(this.OVERRIDE_PREF).then(value => { + if (value === undefined) { + browser.aboutConfigPrefs.setPref(this.OVERRIDE_PREF, true); + } else if (value === false) { + this.unregisterUAOverrides(); + } else { + this.registerUAOverrides(); + } + }); + } + + getAvailableOverrides() { + return this._availableOverrides; + } + + isEnabled() { + return this._overridesEnabled; + } + + enableOverride(override) { + if (override.active) { + return; + } + + const { blocks, matches, telemetryKey, uaTransformer } = override.config; + const listener = details => { + // We set the "used" telemetry key if the user would have had the + // override applied, regardless of whether it is actually applied. + if (!details.frameId && override.shouldSendDetailedTelemetry) { + // For now, we only care about Telemetry on Fennec, where telemetry + // is sent in Java code (as part of the core ping). That code must + // be aware of each key we send, which we send as a SharedPreference. + browser.sharedPreferences.setBoolPref(`${telemetryKey}Used`, true); + } + + // Don't actually override the UA for an experiment if the user is not + // part of the experiment (unless they force-enabed the override). + if ( + !override.config.experiment || + override.experimentActive || + override.permanentPrefEnabled === true + ) { + for (const header of details.requestHeaders) { + if (header.name.toLowerCase() === "user-agent") { + // Don't override the UA if we're on a mobile device that has the + // "Request Desktop Site" mode enabled. The UA for the desktop mode + // is set inside Gecko with a simple string replace, so we can use + // that as a check, see https://searchfox.org/mozilla-central/rev/89d33e1c3b0a57a9377b4815c2f4b58d933b7c32/mobile/android/chrome/geckoview/GeckoViewSettingsChild.js#23-28 + let isMobileWithDesktopMode = + override.currentPlatform == "android" && + header.value.includes("X11; Linux x86_64"); + + if (!isMobileWithDesktopMode) { + header.value = uaTransformer(header.value); + } + } + } + } + return { requestHeaders: details.requestHeaders }; + }; + + browser.webRequest.onBeforeSendHeaders.addListener( + listener, + { urls: matches }, + ["blocking", "requestHeaders"] + ); + + const listeners = { onBeforeSendHeaders: listener }; + if (blocks) { + const blistener = details => { + return { cancel: true }; + }; + + browser.webRequest.onBeforeRequest.addListener( + blistener, + { urls: blocks }, + ["blocking"] + ); + + listeners.onBeforeRequest = blistener; + } + this._activeListeners.set(override, listeners); + override.active = true; + + // If telemetry is being collected, note the addon version. + if (telemetryKey) { + const { version } = browser.runtime.getManifest(); + browser.sharedPreferences.setCharPref(`${telemetryKey}Version`, version); + } + + // If collecting detailed telemetry on the override, note that it was activated. + if (override.shouldSendDetailedTelemetry) { + browser.sharedPreferences.setBoolPref(`${telemetryKey}Ready`, true); + } + } + + onOverrideConfigChanged(override) { + // Check whether the override should be hidden from about:compat. + override.hidden = override.config.hidden; + + // Also hide if the override is in an experiment the user is not part of. + if (override.config.experiment && !override.experimentActive) { + override.hidden = true; + } + + // Setting the override's permanent pref overrules whether it is hidden. + if (override.permanentPrefEnabled !== undefined) { + override.hidden = !override.permanentPrefEnabled; + } + + // Also check whether the override should be active. + let shouldBeActive = true; + + // Overrides can be force-deactivated by their permanent preference. + if (override.permanentPrefEnabled === false) { + shouldBeActive = false; + } + + // Only send detailed telemetry if the user is actively in an experiment or + // has opted into an experimental feature. + override.shouldSendDetailedTelemetry = + override.config.telemetryKey && + (override.experimentActive || override.permanentPrefEnabled); + + // Overrides gated behind an experiment the user is not part of do not + // have to be activated, unless they are gathering telemetry, or the + // user has force-enabled them with their permanent pref. + if ( + override.config.experiment && + !override.experimentActive && + !override.config.telemetryKey && + override.permanentPrefEnabled !== true + ) { + shouldBeActive = false; + } + + if (shouldBeActive) { + this.enableOverride(override); + } else { + this.disableOverride(override); + } + + if (this._overridesEnabled) { + this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({ + overridesChanged: this._aboutCompatBroker.filterOverrides( + this._availableOverrides + ), + }); + } + } + + async registerUAOverrides() { + const platformMatches = ["all"]; + let platformInfo = await browser.runtime.getPlatformInfo(); + platformMatches.push(platformInfo.os == "android" ? "android" : "desktop"); + + for (const override of this._availableOverrides) { + if (platformMatches.includes(override.platform)) { + override.availableOnPlatform = true; + override.currentPlatform = platformInfo.os; + + // Note whether the user is actively in the override's experiment (if any). + override.experimentActive = false; + const experiment = override.config.experiment; + if (experiment) { + // We expect the definition to have either one string for 'experiment' + // (just one branch) or an array of strings (multiple branches). So + // here we turn the string case into a one-element array for the loop. + const branches = Array.isArray(experiment) + ? experiment + : [experiment]; + for (const branch of branches) { + if (await browser.experiments.isActive(branch)) { + override.experimentActive = true; + break; + } + } + } + + // If there is a specific about:config preference governing + // this override, monitor its state. + const pref = override.config.permanentPref; + override.permanentPrefEnabled = + pref && (await browser.aboutConfigPrefs.getPref(pref)); + if (pref) { + const checkOverridePref = () => { + browser.aboutConfigPrefs.getPref(pref).then(value => { + override.permanentPrefEnabled = value; + this.onOverrideConfigChanged(override); + }); + }; + browser.aboutConfigPrefs.onPrefChange.addListener( + checkOverridePref, + pref + ); + } + + this.onOverrideConfigChanged(override); + } + } + + this._overridesEnabled = true; + this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({ + overridesChanged: this._aboutCompatBroker.filterOverrides( + this._availableOverrides + ), + }); + } + + unregisterUAOverrides() { + for (const override of this._availableOverrides) { + this.disableOverride(override); + } + + this._overridesEnabled = false; + this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({ + overridesChanged: false, + }); + } + + disableOverride(override) { + if (!override.active) { + return; + } + + const listeners = this._activeListeners.get(override); + for (const [name, listener] of Object.entries(listeners)) { + browser.webRequest[name].removeListener(listener); + } + override.active = false; + this._activeListeners.delete(override); + } +} + +module.exports = UAOverrides; diff --git a/browser/extensions/webcompat/manifest.json b/browser/extensions/webcompat/manifest.json new file mode 100644 index 0000000000..2f4c84aeed --- /dev/null +++ b/browser/extensions/webcompat/manifest.json @@ -0,0 +1,144 @@ +{ + "manifest_version": 2, + "name": "Web Compatibility Interventions", + "description": "Urgent post-release fixes for web compatibility.", + "version": "19.0.0", + + "applications": { + "gecko": { + "id": "webcompat@mozilla.org", + "strict_min_version": "59.0b5" + } + }, + + "experiment_apis": { + "aboutConfigPrefs": { + "schema": "experiment-apis/aboutConfigPrefs.json", + "parent": { + "scopes": ["addon_parent"], + "script": "experiment-apis/aboutConfigPrefs.js", + "paths": [["aboutConfigPrefs"]] + } + }, + "appConstants": { + "schema": "experiment-apis/appConstants.json", + "parent": { + "scopes": ["addon_parent"], + "script": "experiment-apis/appConstants.js", + "paths": [["appConstants"]] + } + }, + "aboutPage": { + "schema": "about-compat/aboutPage.json", + "parent": { + "scopes": ["addon_parent"], + "script": "about-compat/aboutPage.js", + "events": ["startup"] + } + }, + "experiments": { + "schema": "experiment-apis/experiments.json", + "parent": { + "scopes": ["addon_parent"], + "script": "experiment-apis/experiments.js", + "paths": [["experiments"]] + } + }, + "matchPatterns": { + "schema": "experiment-apis/matchPatterns.json", + "child": { + "scopes": ["addon_child"], + "script": "experiment-apis/matchPatterns.js", + "paths": [["matchPatterns"]] + } + }, + "pictureInPictureChild": { + "schema": "experiment-apis/pictureInPicture.json", + "child": { + "scopes": ["addon_child"], + "script": "experiment-apis/pictureInPicture.js", + "paths": [["pictureInPictureChild"]] + } + }, + "pictureInPictureParent": { + "schema": "experiment-apis/pictureInPicture.json", + "parent": { + "scopes": ["addon_parent"], + "script": "experiment-apis/pictureInPicture.js", + "paths": [["pictureInPictureParent"]] + } + }, + "sharedPreferences": { + "schema": "experiment-apis/sharedPreferences.json", + "parent": { + "scopes": ["addon_parent"], + "script": "experiment-apis/sharedPreferences.js", + "paths": [["sharedPreferences"]] + } + }, + "systemManufacturer": { + "schema": "experiment-apis/systemManufacturer.json", + "child": { + "scopes": ["addon_child"], + "script": "experiment-apis/systemManufacturer.js", + "paths": [["systemManufacturer"]] + } + }, + "trackingProtection": { + "schema": "experiment-apis/trackingProtection.json", + "parent": { + "scopes": ["addon_parent"], + "script": "experiment-apis/trackingProtection.js", + "paths": [["trackingProtection"]] + } + } + }, + + "content_security_policy": "script-src 'self' 'sha256-MmZkN2QaIHhfRWPZ8TVRjijTn5Ci1iEabtTEWrt9CCo='; default-src 'self'; base-uri moz-extension://*; object-src 'none'", + + "permissions": [ + "tabs", + "webNavigation", + "webRequest", + "webRequestBlocking", + "<all_urls>" + ], + + "background": { + "scripts": [ + "lib/module_shim.js", + "lib/messaging_helper.js", + "lib/intervention_helpers.js", + "data/injections.js", + "data/picture_in_picture_overrides.js", + "data/shims.js", + "data/ua_overrides.js", + "lib/about_compat_broker.js", + "lib/custom_functions.js", + "lib/injections.js", + "lib/picture_in_picture_overrides.js", + "lib/shims.js", + "lib/ua_overrides.js", + "run.js" + ] + }, + + "web_accessible_resources": [ + "shims/adsafeprotected-ima.js", + "shims/bmauth.js", + "shims/eluminate.js", + "shims/empty-script.js", + "shims/facebook-sdk.js", + "shims/google-analytics-ecommerce-plugin.js", + "shims/google-analytics-legacy.js", + "shims/google-analytics-tag-manager.js", + "shims/google-analytics.js", + "shims/google-publisher-tags.js", + "shims/live-test-shim.js", + "shims/mochitest-shim-1.js", + "shims/mochitest-shim-2.js", + "shims/mochitest-shim-3.js", + "shims/rambler-authenticator.js", + "shims/rich-relevance.js" + ] +} diff --git a/browser/extensions/webcompat/moz.build b/browser/extensions/webcompat/moz.build new file mode 100644 index 0000000000..624db15176 --- /dev/null +++ b/browser/extensions/webcompat/moz.build @@ -0,0 +1,125 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DEFINES["MOZ_APP_VERSION"] = CONFIG["MOZ_APP_VERSION"] +DEFINES["MOZ_APP_MAXVERSION"] = CONFIG["MOZ_APP_MAXVERSION"] + +FINAL_TARGET_FILES.features["webcompat@mozilla.org"] += [ + "manifest.json", + "run.js", +] + +FINAL_TARGET_FILES.features["webcompat@mozilla.org"]["about-compat"] += [ + "about-compat/aboutCompat.css", + "about-compat/aboutCompat.html", + "about-compat/aboutCompat.js", + "about-compat/AboutCompat.jsm", + "about-compat/aboutPage.js", + "about-compat/aboutPage.json", + "about-compat/aboutPageProcessScript.js", +] + +FINAL_TARGET_FILES.features["webcompat@mozilla.org"]["data"] += [ + "data/injections.js", + "data/picture_in_picture_overrides.js", + "data/shims.js", + "data/ua_overrides.js", +] + +FINAL_TARGET_FILES.features["webcompat@mozilla.org"]["experiment-apis"] += [ + "experiment-apis/aboutConfigPrefs.js", + "experiment-apis/aboutConfigPrefs.json", + "experiment-apis/appConstants.js", + "experiment-apis/appConstants.json", + "experiment-apis/experiments.js", + "experiment-apis/experiments.json", + "experiment-apis/matchPatterns.js", + "experiment-apis/matchPatterns.json", + "experiment-apis/pictureInPicture.js", + "experiment-apis/pictureInPicture.json", + "experiment-apis/sharedPreferences.js", + "experiment-apis/sharedPreferences.json", + "experiment-apis/systemManufacturer.js", + "experiment-apis/systemManufacturer.json", + "experiment-apis/trackingProtection.js", + "experiment-apis/trackingProtection.json", +] + +FINAL_TARGET_FILES.features["webcompat@mozilla.org"]["injections"]["css"] += [ + "injections/css/bug0000000-testbed-css-injection.css", + "injections/css/bug1561371-mail.google.com-allow-horizontal-scrolling.css", + "injections/css/bug1570119-teamcoco.com-scrollbar-width.css", + "injections/css/bug1570328-developer-apple.com-transform-scale.css", + "injections/css/bug1575000-apply.lloydsbank.co.uk-radio-buttons-fix.css", + "injections/css/bug1605611-maps.google.com-directions-time.css", + "injections/css/bug1610016-gaana.com-input-position-fix.css", + "injections/css/bug1610344-directv.com.co-hide-unsupported-message.css", + "injections/css/bug1644830-missingmail.usps.com-checkboxes-not-visible.css", + "injections/css/bug1645064-s-kanava.fi-invisible-charts.css", + "injections/css/bug1651917-teletrader.com.body-transform-origin.css", + "injections/css/bug1653075-livescience.com-scrollbar-width.css", + "injections/css/bug1654865-sports.ndtv.com-float-fix.css", + "injections/css/bug1654877-preev.com-moz-appearance-fix.css", + "injections/css/bug1654907-reactine.ca-hide-unsupported.css", + "injections/css/bug1655049-dev.to-unclickable-button-fix.css", + "injections/css/bug1666771-zilow-map-overdraw.css", +] + +FINAL_TARGET_FILES.features["webcompat@mozilla.org"]["injections"]["js"] += [ + "injections/js/bug0000000-testbed-js-injection.js", + "injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js", + "injections/js/bug1457335-histography.io-ua-change.js", + "injections/js/bug1472075-bankofamerica.com-ua-change.js", + "injections/js/bug1570856-medium.com-menu-isTier1.js", + "injections/js/bug1579159-m.tailieu.vn-pdfjs-worker-disable.js", + "injections/js/bug1605611-maps.google.com-directions-time.js", + "injections/js/bug1610358-pcloud.com-appVersion-change.js", + "injections/js/bug1631811-datastudio.google.com-indexedDB.js", + "injections/js/bug1665035-dckids.com-cookieEnabled.js", + "injections/js/bug1677442-store.hp.com-disable-indexeddb.js", + "injections/js/bug1682238-gamearter.com-ua-change.js", +] + +FINAL_TARGET_FILES.features["webcompat@mozilla.org"]["shims"] += [ + "shims/adsafeprotected-ima.js", + "shims/bmauth.js", + "shims/eluminate.js", + "shims/empty-script.js", + "shims/facebook-sdk.js", + "shims/google-analytics-ecommerce-plugin.js", + "shims/google-analytics-legacy.js", + "shims/google-analytics-tag-manager.js", + "shims/google-analytics.js", + "shims/google-publisher-tags.js", + "shims/live-test-shim.js", + "shims/mochitest-shim-1.js", + "shims/mochitest-shim-2.js", + "shims/mochitest-shim-3.js", + "shims/rambler-authenticator.js", + "shims/rich-relevance.js", +] + +FINAL_TARGET_FILES.features["webcompat@mozilla.org"]["lib"] += [ + "lib/about_compat_broker.js", + "lib/custom_functions.js", + "lib/injections.js", + "lib/intervention_helpers.js", + "lib/messaging_helper.js", + "lib/module_shim.js", + "lib/picture_in_picture_overrides.js", + "lib/shim_messaging_helper.js", + "lib/shims.js", + "lib/ua_overrides.js", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.ini"] + +with Files("**"): + BUG_COMPONENT = ("Web Compatibility", "Tooling & Investigations") diff --git a/browser/extensions/webcompat/run.js b/browser/extensions/webcompat/run.js new file mode 100644 index 0000000000..923f9d7446 --- /dev/null +++ b/browser/extensions/webcompat/run.js @@ -0,0 +1,24 @@ +/* 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 AboutCompatBroker, AVAILABLE_INJECTIONS, AVAILABLE_SHIMS, + AVAILABLE_PIP_OVERRIDES, AVAILABLE_UA_OVERRIDES, CUSTOM_FUNCTIONS, + Injections, PictureInPictureOverrides, Shims, UAOverrides */ + +const injections = new Injections(AVAILABLE_INJECTIONS, CUSTOM_FUNCTIONS); +const uaOverrides = new UAOverrides(AVAILABLE_UA_OVERRIDES); +const pipOverrides = new PictureInPictureOverrides(AVAILABLE_PIP_OVERRIDES); +const shims = new Shims(AVAILABLE_SHIMS); + +const aboutCompatBroker = new AboutCompatBroker({ + injections, + uaOverrides, +}); + +aboutCompatBroker.bootup(); +injections.bootup(); +uaOverrides.bootup(); +pipOverrides.bootup(); diff --git a/browser/extensions/webcompat/shims/adsafeprotected-ima.js b/browser/extensions/webcompat/shims/adsafeprotected-ima.js new file mode 100644 index 0000000000..de273de7d8 --- /dev/null +++ b/browser/extensions/webcompat/shims/adsafeprotected-ima.js @@ -0,0 +1,69 @@ +/* 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"; + +/** + * Bug 1508639 - Shim Ad Safe Protected's Google IMA adapter + */ + +if (!window.googleImaVansAdapter) { + const shimId = "AdSafeProtectedGoogleIMAAdapter"; + + const sendMessageToAddon = (function() { + const pendingMessages = new Map(); + const channel = new MessageChannel(); + channel.port1.onerror = console.error; + channel.port1.onmessage = event => { + const { messageId, response } = event.data; + const resolve = pendingMessages.get(messageId); + if (resolve) { + pendingMessages.delete(messageId); + resolve(response); + } + }; + function reconnect() { + const detail = { + pendingMessages: [...pendingMessages.values()], + port: channel.port2, + shimId, + }; + window.dispatchEvent(new CustomEvent("ShimConnects", { detail })); + } + window.addEventListener("ShimHelperReady", reconnect); + reconnect(); + return function(message) { + const messageId = + Math.random() + .toString(36) + .substring(2) + Date.now().toString(36); + return new Promise(resolve => { + const payload = { + message, + messageId, + shimId, + }; + pendingMessages.set(messageId, resolve); + channel.port1.postMessage(payload); + }); + }; + })(); + + window.googleImaVansAdapter = { + init: () => {}, + dispose: () => {}, + }; + + // Treat it as an opt-in when the user clicks on a video + // TODO: Improve this! It races to tell the bg script to unblock the ad from + // https://pubads.g.doubleclick.net/gampad/ads before the page loads them. + async function click(e) { + if (e.isTrusted && e.target.closest("#video-player")) { + document.documentElement.removeEventListener("click", click, true); + await sendMessageToAddon("optIn"); + // TODO: reload ima3.js? + } + } + document.documentElement.addEventListener("click", click, true); +} diff --git a/browser/extensions/webcompat/shims/bmauth.js b/browser/extensions/webcompat/shims/bmauth.js new file mode 100644 index 0000000000..944f2100d6 --- /dev/null +++ b/browser/extensions/webcompat/shims/bmauth.js @@ -0,0 +1,21 @@ +/* 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"; + +if (!window.BmAuth) { + window.BmAuth = { + init: () => new Promise(() => {}), + handleSignIn: () => { + // TODO: handle this properly! + }, + isAuthenticated: () => Promise.resolve(false), + addListener: () => {}, + api: { + event: { + addListener: () => {}, + }, + }, + }; +} diff --git a/browser/extensions/webcompat/shims/eluminate.js b/browser/extensions/webcompat/shims/eluminate.js new file mode 100644 index 0000000000..863d4ef2da --- /dev/null +++ b/browser/extensions/webcompat/shims/eluminate.js @@ -0,0 +1,68 @@ +/* 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"; + +if (!window.CM_DDX) { + window.CM_DDX = { + domReadyFired: false, + headScripts: true, + dispatcherLoadRequested: false, + firstPassFunctionBinding: false, + BAD_PAGE_ID_ELAPSED_TIMEOUT: 5000, + version: -1, + standalone: false, + test: { + syndicate: true, + testCounter: "", + doTest: false, + newWin: false, + process: () => {}, + }, + partner: {}, + invokeFunctionWhenAvailable: a => { + a(); + }, + gup: d => "", + privacy: { + isDoNotTrackEnabled: () => false, + setDoNotTrack: () => {}, + getDoNotTrack: () => false, + }, + setSubCookie: () => {}, + }; + + const noopfn = () => {}; + const w = window; + w.cmAddShared = noopfn; + w.cmCalcSKUString = noopfn; + w.cmCreateManualImpressionTag = noopfn; + w.cmCreateManualLinkClickTag = noopfn; + w.cmCreateManualPageviewTag = noopfn; + w.cmCreateOrderTag = noopfn; + w.cmCreatePageviewTag = noopfn; + w.cmRetrieveUserID = noopfn; + w.cmSetClientID = noopfn; + w.cmSetCurrencyCode = noopfn; + w.cmSetFirstPartyIDs = noopfn; + w.cmSetSubCookie = noopfn; + w.cmSetupCookieMigration = noopfn; + w.cmSetupNormalization = noopfn; + w.cmSetupOther = noopfn; + w.cmStartTagSet = noopfn; + + function cmExecuteTagQueue() { + var b = window.cmTagQueue; + if (b) { + if (!Array.isArray(b)) { + return undefined; + } + for (var a = 0; a < b.length; ++a) { + window[b[a][0]].apply(window, b[a].slice(1)); + } + } + return true; + } + cmExecuteTagQueue(); +} diff --git a/browser/extensions/webcompat/shims/empty-script.js b/browser/extensions/webcompat/shims/empty-script.js new file mode 100644 index 0000000000..d01f2ab537 --- /dev/null +++ b/browser/extensions/webcompat/shims/empty-script.js @@ -0,0 +1,5 @@ +/* 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/. */ + +/* This script is intentionally empty */ diff --git a/browser/extensions/webcompat/shims/facebook-sdk.js b/browser/extensions/webcompat/shims/facebook-sdk.js new file mode 100644 index 0000000000..95928be857 --- /dev/null +++ b/browser/extensions/webcompat/shims/facebook-sdk.js @@ -0,0 +1,198 @@ +/* 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"; + +/** + * Bug 1226498 - Shim Facebook SDK + * + * The Facebook SDK is commonly used by sites to allow users to authenticate + * for logins, but it is blocked by strict tracking protection. It is possible + * to shim the SDK and allow users to still opt into logging in via Facebook. + * It is also possible to replace any Facebook widgets or comments with + * placeholders that the user may click to opt into loading the content. + */ + +if (!window.FB) { + const originalUrl = document.currentScript.src; + const pendingParses = []; + + function getGUID() { + return ( + Math.random() + .toString(36) + .substring(2) + Date.now().toString(36) + ); + } + + const shimId = "FacebookSDK"; + + const sendMessageToAddon = (function() { + const pendingMessages = new Map(); + const channel = new MessageChannel(); + channel.port1.onerror = console.error; + channel.port1.onmessage = event => { + const { messageId, response } = event.data; + const resolve = pendingMessages.get(messageId); + if (resolve) { + pendingMessages.delete(messageId); + resolve(response); + } + }; + function reconnect() { + const detail = { + pendingMessages: [...pendingMessages.values()], + port: channel.port2, + shimId, + }; + window.dispatchEvent(new CustomEvent("ShimConnects", { detail })); + } + window.addEventListener("ShimHelperReady", reconnect); + reconnect(); + return function(message) { + const messageId = getGUID(); + return new Promise(resolve => { + const payload = { + message, + messageId, + shimId, + }; + pendingMessages.set(messageId, resolve); + channel.port1.postMessage(payload); + }); + }; + })(); + + let ready = false; + let initInfo; + const needPopup = + !/app_runner/.test(window.name) && !/iframe_canvas/.test(window.name); + const popupName = getGUID(); + + if (needPopup) { + const oldWindowOpen = window.open; + window.open = function(href, name, params) { + try { + const url = new URL(href); + if ( + url.protocol === "https:" && + (url.hostname === "m.facebook.com" || + url.hostname === "www.facebook.com") && + url.pathname.endsWith("/oauth") + ) { + name = popupName; + } + } catch (_) {} + return oldWindowOpen.call(window, href, name, params); + }; + } + + async function allowFacebookSDK(callback) { + await sendMessageToAddon("optIn"); + + window.FB = undefined; + const oldInit = window.fbAsyncInit; + window.fbAsyncInit = () => { + ready = true; + if (typeof initInfo !== "undefined") { + window.FB.init(initInfo); + } else if (oldInit) { + oldInit(); + } + if (callback) { + callback(); + } + }; + + const s = document.createElement("script"); + s.src = originalUrl; + await new Promise((resolve, reject) => { + s.onerror = reject; + s.onload = function() { + for (const args of pendingParses) { + window.FB.XFBML.parse.apply(window.FB.XFBML, args); + } + resolve(); + }; + document.head.appendChild(s); + }); + } + + function buildPopupParams() { + const { outerWidth, outerHeight, screenX, screenY } = window; + const { width, height } = window.screen; + const w = Math.min(width, 400); + const h = Math.min(height, 400); + const ua = navigator.userAgent; + const isMobile = ua.includes("Mobile") || ua.includes("Tablet"); + const left = screenX + (screenX < 0 ? width : 0) + (outerWidth - w) / 2; + const top = screenY + (screenY < 0 ? height : 0) + (outerHeight - h) / 2.5; + let params = `left=${left},top=${top},width=${w},height=${h},scrollbars=1,toolbar=0,location=1`; + if (!isMobile) { + params = `${params},width=${w},height=${h}`; + } + return params; + } + + async function doLogin(a, b) { + window.FB.login(a, b); + } + + function proxy(name, fn) { + return function() { + if (ready) { + return window.FB[name].apply(this, arguments); + } + return fn.apply(this, arguments); + }; + } + + window.FB = { + api: proxy("api", () => {}), + AppEvents: { + EventNames: {}, + logPageView: () => {}, + }, + Event: { + subscribe: () => {}, + }, + getAccessToken: proxy("getAccessToken", () => null), + getAuthResponse: proxy("getAuthResponse", () => { + return { status: "" }; + }), + getLoginStatus: proxy("getLoginStatus", cb => { + cb({ status: "" }); + }), + getUserID: proxy("getUserID", () => {}), + init: _initInfo => { + if (ready) { + doLogin(_initInfo); + } else { + initInfo = _initInfo; // in case the site is not using fbAsyncInit + } + }, + login: (a, b) => { + // We have to load Facebook's script, and then wait for it to call + // window.open. By that time, the popup blocker will likely trigger. + // So we open a popup now with about:blank, and then make sure FB + // will re-use that same popup later. + if (needPopup) { + window.open("about:blank", popupName, buildPopupParams()); + } + allowFacebookSDK(() => { + doLogin(a, b); + }); + }, + logout: proxy("logout", cb => cb()), + XFBML: { + parse: e => { + pendingParses.push([e]); + }, + }, + }; + + if (window.fbAsyncInit) { + window.fbAsyncInit(); + } +} diff --git a/browser/extensions/webcompat/shims/google-analytics-ecommerce-plugin.js b/browser/extensions/webcompat/shims/google-analytics-ecommerce-plugin.js new file mode 100644 index 0000000000..60b49df120 --- /dev/null +++ b/browser/extensions/webcompat/shims/google-analytics-ecommerce-plugin.js @@ -0,0 +1,13 @@ +/* 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"; + +if (!window.gaplugins) { + window.gaplugins = {}; +} + +if (!window.gaplugins.EC) { + window.gaplugins.EC = () => {}; +} diff --git a/browser/extensions/webcompat/shims/google-analytics-legacy.js b/browser/extensions/webcompat/shims/google-analytics-legacy.js new file mode 100644 index 0000000000..056186f75c --- /dev/null +++ b/browser/extensions/webcompat/shims/google-analytics-legacy.js @@ -0,0 +1,133 @@ +/* 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/. */ + +// based on https://github.com/gorhill/uBlock/blob/caa8e7d35ba61214a9d13e7d324b2bd2aa73237f/src/web_accessible_resources/google-analytics_ga.js + +"use strict"; + +if (!window._gaq) { + function noopfn() {} + + const gaq = { + Na: noopfn, + O: noopfn, + Sa: noopfn, + Ta: noopfn, + Va: noopfn, + _createAsyncTracker: noopfn, + _getAsyncTracker: noopfn, + _getPlugin: noopfn, + push: a => { + if (typeof a === "function") { + a(); + return; + } + if (!Array.isArray(a)) { + return; + } + if (a[0] === "_link" && typeof a[1] === "string") { + window.location.assign(a[1]); + } + if ( + a[0] === "_set" && + a[1] === "hitCallback" && + typeof a[2] === "function" + ) { + a[2](); + } + }, + }; + + const tracker = { + _addIgnoredOrganic: noopfn, + _addIgnoredRef: noopfn, + _addItem: noopfn, + _addOrganic: noopfn, + _addTrans: noopfn, + _clearIgnoredOrganic: noopfn, + _clearIgnoredRef: noopfn, + _clearOrganic: noopfn, + _cookiePathCopy: noopfn, + _deleteCustomVar: noopfn, + _getName: noopfn, + _setAccount: noopfn, + _getAccount: noopfn, + _getClientInfo: noopfn, + _getDetectFlash: noopfn, + _getDetectTitle: noopfn, + _getLinkerUrl: a => a, + _getLocalGifPath: noopfn, + _getServiceMode: noopfn, + _getVersion: noopfn, + _getVisitorCustomVar: noopfn, + _initData: noopfn, + _link: noopfn, + _linkByPost: noopfn, + _setAllowAnchor: noopfn, + _setAllowHash: noopfn, + _setAllowLinker: noopfn, + _setCampContentKey: noopfn, + _setCampMediumKey: noopfn, + _setCampNameKey: noopfn, + _setCampNOKey: noopfn, + _setCampSourceKey: noopfn, + _setCampTermKey: noopfn, + _setCampaignCookieTimeout: noopfn, + _setCampaignTrack: noopfn, + _setClientInfo: noopfn, + _setCookiePath: noopfn, + _setCookiePersistence: noopfn, + _setCookieTimeout: noopfn, + _setCustomVar: noopfn, + _setDetectFlash: noopfn, + _setDetectTitle: noopfn, + _setDomainName: noopfn, + _setLocalGifPath: noopfn, + _setLocalRemoteServerMode: noopfn, + _setLocalServerMode: noopfn, + _setReferrerOverride: noopfn, + _setRemoteServerMode: noopfn, + _setSampleRate: noopfn, + _setSessionTimeout: noopfn, + _setSiteSpeedSampleRate: noopfn, + _setSessionCookieTimeout: noopfn, + _setVar: noopfn, + _setVisitorCookieTimeout: noopfn, + _trackEvent: noopfn, + _trackPageLoadTime: noopfn, + _trackPageview: noopfn, + _trackSocial: noopfn, + _trackTiming: noopfn, + _trackTrans: noopfn, + _visitCode: noopfn, + }; + + const gat = { + _anonymizeIP: noopfn, + _createTracker: noopfn, + _forceSSL: noopfn, + _getPlugin: noopfn, + _getTracker: () => tracker, + _getTrackerByName: () => tracker, + _getTrackers: noopfn, + aa: noopfn, + ab: noopfn, + hb: noopfn, + la: noopfn, + oa: noopfn, + pa: noopfn, + u: noopfn, + }; + + window._gat = gat; + + const aa = window._gaq || []; + if (Array.isArray(aa)) { + while (aa[0]) { + gaq.push(aa.shift()); + } + } + + window._gaq = gaq.qf = gaq; +} diff --git a/browser/extensions/webcompat/shims/google-analytics-tag-manager.js b/browser/extensions/webcompat/shims/google-analytics-tag-manager.js new file mode 100644 index 0000000000..b8398f91b6 --- /dev/null +++ b/browser/extensions/webcompat/shims/google-analytics-tag-manager.js @@ -0,0 +1,24 @@ +/* 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/. */ + +// based on https://github.com/gorhill/uBlock/blob/caa8e7d35ba61214a9d13e7d324b2bd2aa73237f/src/web_accessible_resources/googletagmanager_gtm.js + +"use strict"; + +if (!window.ga) { + window.ga = () => {}; + + try { + window.dataLayer.hide.end(); + } catch (_) {} + + const dl = window.dataLayer; + if (typeof dl.push === "function") { + dl.push = o => { + if (o instanceof Object && typeof o.eventCallback === "function") { + setTimeout(o.eventCallback, 1); + } + }; + } +} diff --git a/browser/extensions/webcompat/shims/google-analytics.js b/browser/extensions/webcompat/shims/google-analytics.js new file mode 100644 index 0000000000..f0162b1f63 --- /dev/null +++ b/browser/extensions/webcompat/shims/google-analytics.js @@ -0,0 +1,45 @@ +/* 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/. */ + +// based on https://github.com/gorhill/uBlock/blob/8a1a8b103f56e4fcef1264e02dfd718a29bda006/src/web_accessible_resources/google-analytics_analytics.js + +"use strict"; + +if (!window[window.GoogleAnalyticsObject || "ga"]) { + function ga() { + const len = arguments.length; + if (!len) { + return; + } + const args = Array.from(arguments); + let fn; + let a = args[len - 1]; + if (a instanceof Object && a.hitCallback instanceof Function) { + fn = a.hitCallback; + } else { + const pos = args.indexOf("hitCallback"); + if (pos !== -1 && args[pos + 1] instanceof Function) { + fn = args[pos + 1]; + } + } + if (!(fn instanceof Function)) { + return; + } + try { + fn(); + } catch (_) {} + } + ga.create = () => {}; + ga.getByName = () => null; + ga.getAll = () => []; + ga.remove = () => {}; + ga.loaded = true; + + const gaName = window.GoogleAnalyticsObject || "ga"; + window[gaName] = ga; +} + +try { + window.dataLayer.hide.end(); +} catch (_) {} diff --git a/browser/extensions/webcompat/shims/google-publisher-tags.js b/browser/extensions/webcompat/shims/google-publisher-tags.js new file mode 100644 index 0000000000..756e29bac5 --- /dev/null +++ b/browser/extensions/webcompat/shims/google-publisher-tags.js @@ -0,0 +1,163 @@ +/* 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"; + +/** + * Bug 1600538 - Shim Google Publisher Tags + */ + +"use strict"; + +if (!window.googletag?.apiReady) { + const noopfn = function() {}; + const noopthisfn = function() { + return this; + }; + const noopnullfn = function() { + return null; + }; + const nooparrayfn = function() { + return []; + }; + const noopstrfn = function() { + return ""; + }; + + function newPassbackSlot() { + return { + display: noopfn, + get: noopnullfn, + set: noopthisfn, + setClickUrl: noopthisfn, + setTagForChildDirectedTreatment: noopthisfn, + setTargeting: noopthisfn, + updateTargetingFromMap: noopthisfn, + }; + } + + function display(id) { + const parent = document.getElementById(id); + if (parent) { + parent.appendChild(document.createElement("div")); + } + } + + const companionAdsService = { + addEventListener: noopthisfn, + enableSyncLoading: noopfn, + setRefreshUnfilledSlots: noopfn, + }; + + const contentService = { + addEventListener: noopthisfn, + setContent: noopfn, + }; + + const pubadsService = { + addEventListener: noopthisfn, + clear: noopfn, + clearCategoryExclusions: noopthisfn, + clearTagForChildDirectedTreatment: noopthisfn, + clearTargeting: noopthisfn, + collapseEmptyDivs: noopfn, + defineOutOfPagePassback: () => newPassbackSlot(), + definePassback: () => newPassbackSlot(), + disableInitialLoad: noopfn, + display, + enableAsyncRendering: noopfn, + enableSingleRequest: noopfn, + enableSyncRendering: noopfn, + enableVideoAds: noopfn, + get: noopnullfn, + getAttributeKeys: nooparrayfn, + getTargeting: noopfn, + getTargetingKeys: nooparrayfn, + getSlots: nooparrayfn, + refresh: noopfn, + set: noopthisfn, + setCategoryExclusion: noopthisfn, + setCentering: noopfn, + setCookieOptions: noopthisfn, + setForceSafeFrame: noopthisfn, + setLocation: noopthisfn, + setPublisherProvidedId: noopthisfn, + setRequestNonPersonalizedAds: noopthisfn, + setSafeFrameConfig: noopthisfn, + setTagForChildDirectedTreatment: noopthisfn, + setTargeting: noopthisfn, + setVideoContent: noopthisfn, + updateCorrelator: noopfn, + }; + + function newSizeMappingBuilder() { + return { + addSize: noopthisfn, + build: noopnullfn, + }; + } + + function newSlot() { + return { + addService: noopthisfn, + clearCategoryExclusions: noopthisfn, + clearTargeting: noopthisfn, + defineSizeMapping: noopthisfn, + get: noopnullfn, + getAdUnitPath: nooparrayfn, + getAttributeKeys: nooparrayfn, + getCategoryExclusions: nooparrayfn, + getDomId: noopstrfn, + getSlotElementId: noopstrfn, + getSlotId: noopthisfn, + getTargeting: nooparrayfn, + getTargetingKeys: nooparrayfn, + set: noopthisfn, + setCategoryExclusion: noopthisfn, + setClickUrl: noopthisfn, + setCollapseEmptyDiv: noopthisfn, + setTargeting: noopthisfn, + }; + } + + let gt = window.googletag; + if (!gt) { + gt = window.googletag = {}; + } + + for (const [key, value] of Object.entries({ + apiReady: true, + companionAds: () => companionAdsService, + content: () => contentService, + defineOutOfPageSlot: () => newSlot(), + defineSlot: () => newSlot(), + destroySlots: noopfn, + disablePublisherConsole: noopfn, + display, + enableServices: noopfn, + getVersion: noopstrfn, + pubads: () => pubadsService, + pubabsReady: true, + setAdIframeTitle: noopfn, + sizeMapping: () => newSizeMappingBuilder(), + })) { + gt[key] = value; + } + + function runCmd(fn) { + try { + fn(); + } catch (_) {} + return 1; + } + + const cmds = gt.cmd; + const newCmd = []; + newCmd.push = runCmd; + gt.cmd = newCmd; + + for (const cmd of cmds) { + runCmd(cmd); + } +} diff --git a/browser/extensions/webcompat/shims/live-test-shim.js b/browser/extensions/webcompat/shims/live-test-shim.js new file mode 100644 index 0000000000..552020820f --- /dev/null +++ b/browser/extensions/webcompat/shims/live-test-shim.js @@ -0,0 +1,84 @@ +/* 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 */ + +if (!window.LiveTestShimPromise) { + const originalUrl = document.currentScript.src; + + const shimId = "LiveTestShim"; + + const sendMessageToAddon = (function() { + const pendingMessages = new Map(); + const channel = new MessageChannel(); + channel.port1.onerror = console.error; + channel.port1.onmessage = event => { + const { messageId, response } = event.data; + const resolve = pendingMessages.get(messageId); + if (resolve) { + pendingMessages.delete(messageId); + resolve(response); + } + }; + function reconnect() { + const detail = { + pendingMessages: [...pendingMessages.values()], + port: channel.port2, + shimId, + }; + window.dispatchEvent(new CustomEvent("ShimConnects", { detail })); + } + window.addEventListener("ShimHelperReady", reconnect); + reconnect(); + return function(message) { + const messageId = + Math.random() + .toString(36) + .substring(2) + Date.now().toString(36); + return new Promise(resolve => { + const payload = { + message, + messageId, + shimId, + }; + pendingMessages.set(messageId, resolve); + channel.port1.postMessage(payload); + }); + }; + })(); + + async function go(options) { + try { + const o = document.getElementById("shims"); + const cl = o.classList; + cl.remove("red"); + cl.add("green"); + o.innerText = JSON.stringify(options || ""); + } catch (_) {} + + if (window !== top) { + return; + } + + await sendMessageToAddon("optIn"); + + const s = document.createElement("script"); + s.src = originalUrl; + document.head.appendChild(s); + } + + window[`${shimId}Promise`] = sendMessageToAddon("getOptions").then( + options => { + if (document.readyState !== "loading") { + go(options); + } else { + window.addEventListener("DOMContentLoaded", () => { + go(options); + }); + } + } + ); +} diff --git a/browser/extensions/webcompat/shims/mochitest-shim-1.js b/browser/extensions/webcompat/shims/mochitest-shim-1.js new file mode 100644 index 0000000000..d18e965f3c --- /dev/null +++ b/browser/extensions/webcompat/shims/mochitest-shim-1.js @@ -0,0 +1,89 @@ +/* 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 */ + +if (!window.MochitestShimPromise) { + const originalUrl = document.currentScript.src; + + const shimId = "MochitestShim"; + + const sendMessageToAddon = (function() { + const pendingMessages = new Map(); + const channel = new MessageChannel(); + channel.port1.onerror = console.error; + channel.port1.onmessage = event => { + const { messageId, response } = event.data; + const resolve = pendingMessages.get(messageId); + if (resolve) { + pendingMessages.delete(messageId); + resolve(response); + } + }; + function reconnect() { + const detail = { + pendingMessages: [...pendingMessages.values()], + port: channel.port2, + shimId, + }; + window.dispatchEvent(new CustomEvent("ShimConnects", { detail })); + } + window.addEventListener("ShimHelperReady", reconnect); + reconnect(); + return function(message) { + const messageId = + Math.random() + .toString(36) + .substring(2) + Date.now().toString(36); + return new Promise(resolve => { + const payload = { + message, + messageId, + shimId, + }; + pendingMessages.set(messageId, resolve); + channel.port1.postMessage(payload); + }); + }; + })(); + + async function go(options) { + try { + const o = document.getElementById("shims"); + const cl = o.classList; + cl.remove("red"); + cl.add("green"); + o.innerText = JSON.stringify(options || ""); + } catch (_) {} + + window.shimPromiseResolve("shimmed"); + + if (window !== top) { + window.optInPromiseResolve(false); + return; + } + + await sendMessageToAddon("optIn"); + + window.doingOptIn = true; + const s = document.createElement("script"); + s.src = originalUrl; + s.onerror = () => window.optInPromiseResolve("error"); + document.head.appendChild(s); + } + + window[`${shimId}Promise`] = new Promise(resolve => { + sendMessageToAddon("getOptions").then(options => { + if (document.readyState !== "loading") { + resolve(go(options)); + } else { + window.addEventListener("DOMContentLoaded", () => { + resolve(go(options)); + }); + } + }); + }); +} diff --git a/browser/extensions/webcompat/shims/mochitest-shim-2.js b/browser/extensions/webcompat/shims/mochitest-shim-2.js new file mode 100644 index 0000000000..3b60038599 --- /dev/null +++ b/browser/extensions/webcompat/shims/mochitest-shim-2.js @@ -0,0 +1,87 @@ +/* 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 */ + +if (!window.testPromise) { + const originalUrl = document.currentScript.src; + + const shimId = "MochitestShim2"; + + const sendMessageToAddon = (function() { + const pendingMessages = new Map(); + const channel = new MessageChannel(); + channel.port1.onerror = console.error; + channel.port1.onmessage = event => { + const { messageId, response } = event.data; + const resolve = pendingMessages.get(messageId); + if (resolve) { + pendingMessages.delete(messageId); + resolve(response); + } + }; + function reconnect() { + const detail = { + pendingMessages: [...pendingMessages.values()], + port: channel.port2, + shimId, + }; + window.dispatchEvent(new CustomEvent("ShimConnects", { detail })); + } + window.addEventListener("ShimHelperReady", reconnect); + reconnect(); + return function(message) { + const messageId = + Math.random() + .toString(36) + .substring(2) + Date.now().toString(36); + return new Promise(resolve => { + const payload = { + message, + messageId, + shimId, + }; + pendingMessages.set(messageId, resolve); + channel.port1.postMessage(payload); + }); + }; + })(); + + async function go(options) { + try { + const o = document.getElementById("shims"); + const cl = o.classList; + cl.remove("red"); + cl.add("green"); + o.innerText = JSON.stringify(options || ""); + } catch (_) {} + + window.shimPromiseResolve("shimmed"); + + if (window !== top) { + window.optInPromiseResolve(false); + return; + } + + await sendMessageToAddon("optIn"); + + window.doingOptIn = true; + const s = document.createElement("script"); + s.src = originalUrl; + s.onerror = () => window.optInPromiseResolve("error"); + document.head.appendChild(s); + } + + sendMessageToAddon("getOptions").then(options => { + if (document.readyState !== "loading") { + go(options); + } else { + window.addEventListener("DOMContentLoaded", () => { + go(options); + }); + } + }); +} diff --git a/browser/extensions/webcompat/shims/mochitest-shim-3.js b/browser/extensions/webcompat/shims/mochitest-shim-3.js new file mode 100644 index 0000000000..dc0a8005f5 --- /dev/null +++ b/browser/extensions/webcompat/shims/mochitest-shim-3.js @@ -0,0 +1,7 @@ +/* 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"; + +window.shimPromiseResolve("shimmed"); diff --git a/browser/extensions/webcompat/shims/rambler-authenticator.js b/browser/extensions/webcompat/shims/rambler-authenticator.js new file mode 100644 index 0000000000..0554eabc2c --- /dev/null +++ b/browser/extensions/webcompat/shims/rambler-authenticator.js @@ -0,0 +1,86 @@ +/* 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"; + +if (!window.ramblerIdHelper) { + const originalScript = document.currentScript.src; + + const sendMessageToAddon = (function() { + const shimId = "Rambler"; + const pendingMessages = new Map(); + const channel = new MessageChannel(); + channel.port1.onerror = console.error; + channel.port1.onmessage = event => { + const { messageId, response } = event.data; + const resolve = pendingMessages.get(messageId); + if (resolve) { + pendingMessages.delete(messageId); + resolve(response); + } + }; + function reconnect() { + const detail = { + pendingMessages: [...pendingMessages.values()], + port: channel.port2, + shimId, + }; + window.dispatchEvent(new CustomEvent("ShimConnects", { detail })); + } + window.addEventListener("ShimHelperReady", reconnect); + reconnect(); + return function(message) { + const messageId = + Math.random() + .toString(36) + .substring(2) + Date.now().toString(36); + return new Promise(resolve => { + const payload = { + message, + messageId, + shimId, + }; + pendingMessages.set(messageId, resolve); + channel.port1.postMessage(payload); + }); + }; + })(); + + const ramblerIdHelper = { + getProfileInfo: (successCallback, errorCallback) => { + successCallback({}); + }, + openAuth: () => { + sendMessageToAddon("optIn").then(function() { + const openAuthArgs = arguments; + window.ramblerIdHelper = undefined; + const s = document.createElement("script"); + s.src = originalScript; + document.head.appendChild(s); + s.addEventListener("load", () => { + const helper = window.ramblerIdHelper; + for (const { fn, args } of callLog) { + helper[fn].apply(helper, args); + } + helper.openAuth.apply(helper, openAuthArgs); + }); + }); + }, + }; + + const callLog = []; + function addLoggedCall(fn) { + ramblerIdHelper[fn] = () => { + callLog.push({ fn, args: arguments }); + }; + } + + addLoggedCall("registerOnFrameCloseCallback"); + addLoggedCall("registerOnFrameRedirect"); + addLoggedCall("registerOnPossibleLoginCallback"); + addLoggedCall("registerOnPossibleLogoutCallback"); + addLoggedCall("registerOnPossibleOauthLoginCallback"); + + window.ramblerIdHelper = ramblerIdHelper; +} diff --git a/browser/extensions/webcompat/shims/rich-relevance.js b/browser/extensions/webcompat/shims/rich-relevance.js new file mode 100644 index 0000000000..d7f2802ac9 --- /dev/null +++ b/browser/extensions/webcompat/shims/rich-relevance.js @@ -0,0 +1,30 @@ +/* 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"; + +/** + * Bug 1449347 - Rich Relevance + */ + +"use strict"; + +if (!window.r3_common) { + const noopfn = () => {}; + + window.rr_flush_onload = noopfn; + window.r3 = noopfn; + window.r3_home = noopfn; + window.RR = noopfn; + window.r3_common = function() {}; + window.r3_common.prototype = { + addContext: noopfn, + addPlacementType: noopfn, + setUserId: noopfn, + setSessionId: noopfn, + setClickthruServer: noopfn, + setBaseUrl: noopfn, + setApiKey: noopfn, + }; +} diff --git a/browser/extensions/webcompat/tests/browser/browser.ini b/browser/extensions/webcompat/tests/browser/browser.ini new file mode 100644 index 0000000000..2a7bc6800d --- /dev/null +++ b/browser/extensions/webcompat/tests/browser/browser.ini @@ -0,0 +1,13 @@ +[DEFAULT] +support-files = + head.js + shims_test.js + shims_test_2.js + shims_test_3.js + iframe_test.html + shims_test.html + shims_test_2.html + shims_test_3.html + +[browser_shims.js] +skip-if = verify diff --git a/browser/extensions/webcompat/tests/browser/browser_shims.js b/browser/extensions/webcompat/tests/browser/browser_shims.js new file mode 100644 index 0000000000..cd07bb7390 --- /dev/null +++ b/browser/extensions/webcompat/tests/browser/browser_shims.js @@ -0,0 +1,73 @@ +"use strict"; + +registerCleanupFunction(() => { + UrlClassifierTestUtils.cleanupTestTrackers(); + Services.prefs.clearUserPref(TRACKING_PREF); +}); + +add_task(async function setup() { + await UrlClassifierTestUtils.addTestTrackers(); +}); + +add_task(async function test_shim_disabled_by_own_pref() { + // Test that a shim will not apply if disabled in about:config + + Services.prefs.setBoolPref(DISABLE_SHIM1_PREF, true); + Services.prefs.setBoolPref(TRACKING_PREF, true); + + await testShimDoesNotRun(); + + Services.prefs.clearUserPref(DISABLE_SHIM1_PREF); + Services.prefs.clearUserPref(TRACKING_PREF); +}); + +add_task(async function test_shim_disabled_by_global_pref() { + // Test that a shim will not apply if disabled in about:config + + Services.prefs.setBoolPref(GLOBAL_PREF, false); + Services.prefs.setBoolPref(DISABLE_SHIM1_PREF, false); + Services.prefs.setBoolPref(TRACKING_PREF, true); + + await testShimDoesNotRun(); + + Services.prefs.clearUserPref(GLOBAL_PREF); + Services.prefs.clearUserPref(DISABLE_SHIM1_PREF); + Services.prefs.clearUserPref(TRACKING_PREF); +}); + +add_task(async function test_shim_disabled_hosts_notHosts() { + Services.prefs.setBoolPref(TRACKING_PREF, true); + + await testShimDoesNotRun(false, SHIMMABLE_TEST_PAGE_3); + + Services.prefs.clearUserPref(TRACKING_PREF); +}); + +add_task(async function test_shim_disabled_overridden_by_pref() { + Services.prefs.setBoolPref(TRACKING_PREF, true); + + await testShimDoesNotRun(false, SHIMMABLE_TEST_PAGE_2); + + Services.prefs.setBoolPref(DISABLE_SHIM2_PREF, false); + + await testShimRuns(SHIMMABLE_TEST_PAGE_2); + + Services.prefs.clearUserPref(TRACKING_PREF); + Services.prefs.clearUserPref(DISABLE_SHIM2_PREF); +}); + +add_task(async function test_shim() { + // Test that a shim which only runs in strict mode works, and that it + // is permitted to opt into showing normally-blocked tracking content. + + Services.prefs.setBoolPref(TRACKING_PREF, true); + + await testShimRuns(SHIMMABLE_TEST_PAGE); + + // test that if the user opts in on one domain, they will still have to opt + // in on another domain which embeds an iframe to the first one. + + await testShimRuns(EMBEDDING_TEST_PAGE, 0, false, false); + + Services.prefs.clearUserPref(TRACKING_PREF); +}); diff --git a/browser/extensions/webcompat/tests/browser/head.js b/browser/extensions/webcompat/tests/browser/head.js new file mode 100644 index 0000000000..7bd1dde950 --- /dev/null +++ b/browser/extensions/webcompat/tests/browser/head.js @@ -0,0 +1,139 @@ +"use strict"; + +const TEST_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" +); + +const THIRD_PARTY_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.net" +); + +const SHIMMABLE_TEST_PAGE = `${TEST_ROOT}shims_test.html`; +const SHIMMABLE_TEST_PAGE_2 = `${TEST_ROOT}shims_test_2.html`; +const SHIMMABLE_TEST_PAGE_3 = `${TEST_ROOT}shims_test_3.html`; +const EMBEDDING_TEST_PAGE = `${THIRD_PARTY_ROOT}iframe_test.html`; + +const BLOCKED_TRACKER_URL = + "//trackertest.org/tests/toolkit/components/url-classifier/tests/mochitest/evil.js"; + +const DISABLE_SHIM1_PREF = "extensions.webcompat.disabled_shims.MochitestShim"; +const DISABLE_SHIM2_PREF = "extensions.webcompat.disabled_shims.MochitestShim2"; +const DISABLE_SHIM3_PREF = "extensions.webcompat.disabled_shims.MochitestShim3"; +const DISABLE_SHIM4_PREF = "extensions.webcompat.disabled_shims.MochitestShim4"; +const GLOBAL_PREF = "extensions.webcompat.enable_shims"; +const TRACKING_PREF = "privacy.trackingprotection.enabled"; + +const { UrlClassifierTestUtils } = ChromeUtils.import( + "resource://testing-common/UrlClassifierTestUtils.jsm" +); + +async function testShimRuns( + testPage, + frame, + trackersAllowed = true, + expectOptIn = true +) { + const tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: testPage, + waitForLoad: true, + }); + + const TrackingProtection = tab.ownerGlobal.TrackingProtection; + ok(TrackingProtection, "TP is attached to the tab"); + ok(TrackingProtection.enabled, "TP is enabled"); + + await SpecialPowers.spawn( + tab.linkedBrowser, + [[trackersAllowed, BLOCKED_TRACKER_URL, expectOptIn], frame], + async (args, _frame) => { + const window = _frame === undefined ? content : content.frames[_frame]; + + await SpecialPowers.spawn( + window, + args, + async (_trackersAllowed, trackerUrl, _expectOptIn) => { + const shimResult = await content.wrappedJSObject.shimPromise; + is("shimmed", shimResult, "Shim activated"); + + const optInResult = await content.wrappedJSObject.optInPromise; + is(_expectOptIn, optInResult, "Shim allowed opt in if appropriate"); + + const o = content.document.getElementById("shims"); + const cl = o.classList; + const opts = JSON.parse(o.innerText); + is( + undefined, + opts.branchValue, + "Shim script did not receive option for other branch" + ); + is( + undefined, + opts.platformValue, + "Shim script did not receive option for other platform" + ); + is( + true, + opts.simpleOption, + "Shim script received simple option correctly" + ); + ok(opts.complexOption, "Shim script received complex option"); + is( + 1, + opts.complexOption.a, + "Shim script received complex options correctly #1" + ); + is( + "test", + opts.complexOption.b, + "Shim script received complex options correctly #2" + ); + ok(cl.contains("green"), "Shim affected page correctly"); + } + ); + } + ); + + await BrowserTestUtils.removeTab(tab); +} + +async function testShimDoesNotRun( + trackersAllowed = false, + testPage = SHIMMABLE_TEST_PAGE +) { + const tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: testPage, + waitForLoad: true, + }); + + await SpecialPowers.spawn( + tab.linkedBrowser, + [trackersAllowed, BLOCKED_TRACKER_URL], + async (_trackersAllowed, trackerUrl) => { + const shimResult = await content.wrappedJSObject.shimPromise; + is("did not shim", shimResult, "Shim did not activate"); + + ok( + !content.document.getElementById("shims").classList.contains("green"), + "Shim script did not run" + ); + + is( + _trackersAllowed ? "ALLOWED" : "BLOCKED", + await new Promise(resolve => { + const s = content.document.createElement("script"); + s.src = trackerUrl; + s.onload = () => resolve("ALLOWED"); + s.onerror = () => resolve("BLOCKED"); + content.document.head.appendChild(s); + }), + "Normally-blocked resources blocked if appropriate" + ); + } + ); + + await BrowserTestUtils.removeTab(tab); +} diff --git a/browser/extensions/webcompat/tests/browser/iframe_test.html b/browser/extensions/webcompat/tests/browser/iframe_test.html new file mode 100644 index 0000000000..bf86cc3b0b --- /dev/null +++ b/browser/extensions/webcompat/tests/browser/iframe_test.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf8"> + <script> + window.shimPromise = new Promise(resolve => { + window.shimPromiseResolve = resolve; + }); + window.optInPromise = new Promise(resolve => { + window.optInPromiseResolve = resolve; + }); + </script> + </head> + <body> + <iframe src="http://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test.html"></iframe> + </body> +</html> diff --git a/browser/extensions/webcompat/tests/browser/shims_test.html b/browser/extensions/webcompat/tests/browser/shims_test.html new file mode 100644 index 0000000000..9c0204615c --- /dev/null +++ b/browser/extensions/webcompat/tests/browser/shims_test.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf8"> + <script> + window.shimPromise = new Promise(resolve => { + window.shimPromiseResolve = resolve; + }); + window.optInPromise = new Promise(resolve => { + window.optInPromiseResolve = resolve; + }); + </script> + <script onerror="window.shimPromiseResolve('error')" src="shims_test.js"></script> + </head> + <body> + <div id="shims"></div> + </body> +</html> diff --git a/browser/extensions/webcompat/tests/browser/shims_test.js b/browser/extensions/webcompat/tests/browser/shims_test.js new file mode 100644 index 0000000000..4a55bee7ed --- /dev/null +++ b/browser/extensions/webcompat/tests/browser/shims_test.js @@ -0,0 +1,11 @@ +/* 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"; + +if (window.doingOptIn) { + window.optInPromiseResolve(true); +} else { + window.shimPromiseResolve("did not shim"); +} diff --git a/browser/extensions/webcompat/tests/browser/shims_test_2.html b/browser/extensions/webcompat/tests/browser/shims_test_2.html new file mode 100644 index 0000000000..38dfc26208 --- /dev/null +++ b/browser/extensions/webcompat/tests/browser/shims_test_2.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf8"> + <script> + window.shimPromise = new Promise(resolve => { + window.shimPromiseResolve = resolve; + }); + window.optInPromise = new Promise(resolve => { + window.optInPromiseResolve = resolve; + }); + </script> + <script onerror="window.shimPromiseResolve('error')" src="shims_test_2.js"></script> + </head> + <body> + <div id="shims"></div> + </body> +</html> diff --git a/browser/extensions/webcompat/tests/browser/shims_test_2.js b/browser/extensions/webcompat/tests/browser/shims_test_2.js new file mode 100644 index 0000000000..4a55bee7ed --- /dev/null +++ b/browser/extensions/webcompat/tests/browser/shims_test_2.js @@ -0,0 +1,11 @@ +/* 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"; + +if (window.doingOptIn) { + window.optInPromiseResolve(true); +} else { + window.shimPromiseResolve("did not shim"); +} diff --git a/browser/extensions/webcompat/tests/browser/shims_test_3.html b/browser/extensions/webcompat/tests/browser/shims_test_3.html new file mode 100644 index 0000000000..7f8848d447 --- /dev/null +++ b/browser/extensions/webcompat/tests/browser/shims_test_3.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf8"> + <script> + window.shimPromise = new Promise(resolve => { + window.shimPromiseResolve = resolve; + }); + window.optInPromise = new Promise(resolve => { + window.optInPromiseResolve = resolve; + }); + </script> + <script onerror="window.shimPromiseResolve('error')" src="shims_test_3.js"></script> + </head> + <body> + <div id="shims"></div> + </body> +</html> diff --git a/browser/extensions/webcompat/tests/browser/shims_test_3.js b/browser/extensions/webcompat/tests/browser/shims_test_3.js new file mode 100644 index 0000000000..9acb6cdcf1 --- /dev/null +++ b/browser/extensions/webcompat/tests/browser/shims_test_3.js @@ -0,0 +1,7 @@ +/* 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"; + +window.shimPromiseResolve("did not shim"); |