diff options
Diffstat (limited to '')
7 files changed, 649 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..7c844726f8 --- /dev/null +++ b/browser/extensions/webcompat/about-compat/AboutCompat.jsm @@ -0,0 +1,42 @@ +/* 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 = + globalThis.Services || + ChromeUtils.import("resource://gre/modules/Services.jsm").Services; + +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 | + Ci.nsIAboutModule.IS_SECURE_CHROME_UI + ); + }, + + 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 || + // Handles fallback to earlier versions. + // eslint-disable-next-line mozilla/valid-services-property + 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..b51db7f9f5 --- /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; + border-spacing: 0; + } + + td { + border-bottom: 1px solid var(--in-content-border-color); + } + + td:last-child > button { + float: inline-end; + } +} + +/* Mobile UI where common.css is not loaded */ + +@media (any-pointer: coarse), (any-pointer: none) { + * { + margin: 0; + padding: 0; + } + + :root { + --background-color: #fff; + --text-color: #0c0c0d; + --border-color: #e1e1e2; + --button-background-color: #f5f5f5; + --selected-tab-text-color: #0061e0; + } + + @media (prefers-color-scheme: dark) { + :root { + --background-color: #292833; + --text-color: #f9f9fa; + --border-color: rgba(255, 255, 255, 0.15); + --button-background-color: rgba(0, 0, 0, 0.15); + --selected-tab-text-color: #00ddff; + } + } + + body { + background-color: var(--background-color); + color: var(--text-color); + font: message-box; + font-size: 14px; + -moz-text-size-adjust: none; + display: grid; + grid-template-areas: "a b c" "d d d"; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: fit-content(100%) 1fr; + } + + .tab[data-l10n-id="label-overrides"] { + grid-area: a; + } + + .tab[data-l10n-id="label-interventions"] { + grid-area: b; + } + + .tab[data-l10n-id="label-smartblock"] { + grid-area: c; + } + + table { + grid-area: d; + } + + table, + tr, + p { + display: block; + } + + table { + border-top: 2px solid var(--border-color); + margin-top: -2px; + width: 100%; + z-index: 1; + display: none; + } + + tr { + border-bottom: 1px solid var(--border-color); + padding: 0; + } + + a { + color: inherit; + font-size: 94%; + } + + .tab { + cursor: pointer; + z-index: 2; + display: inline-block; + text-align: left; + border-block: 2px solid transparent; + font-size: 1em; + font-weight: bold; + padding: 1em; + } + + .tab.active { + color: var(--selected-tab-text-color); + border-bottom-color: currentColor; + margin-bottom: 0; + padding-bottom: calc(1em + 2px); + } + + .tab.active + table { + display: block; + } + + td { + grid-area: b; + padding-left: 1em; + } + + td:first-child { + grid-area: a; + padding-top: 1em; + } + + td:last-child { + grid-area: c; + padding-bottom: 1em; + } + + tr { + display: grid; + grid-template-areas: "a c" "b c"; + grid-template-columns: 1fr 6.5em; + } + + td[colspan="4"] { + padding: 1em; + font-style: italic; + text-align: center; + } + + td:not([colspan]):nth-child(1) { + font-weight: bold; + padding-bottom: 0.25em; + } + + td:nth-child(2) { + padding-bottom: 1em; + } + + td:nth-child(3) { + display: flex; + padding: 0; + } + + button { + cursor: pointer; + width: 100%; + height: 100%; + background: var(--button-background-color); + color: inherit; + inset-inline-end: 0; + margin: 0; + padding: 0; + border: 0; + border-inline-start: 1px solid var(--border-color); + font-weight: 600; + appearance: none; + } + + 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..d820f20ee2 --- /dev/null +++ b/browser/extensions/webcompat/about-compat/aboutCompat.html @@ -0,0 +1,51 @@ +<!-- 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="color-scheme" content="light dark" /> + <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> + <h2 class="tab" data-l10n-id="label-smartblock"></h2> + <table id="smartblock" class="shims"> + <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..e01b853877 --- /dev/null +++ b/browser/extensions/webcompat/about-compat/aboutCompat.js @@ -0,0 +1,283 @@ +/* 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("getAllInterventions"), + 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) { + const alsoShowHidden = location.hash === "#all"; + + if ("interventionsChanged" in msg) { + redrawTable($("#interventions"), msg.interventionsChanged, alsoShowHidden); + } + + if ("overridesChanged" in msg) { + redrawTable($("#overrides"), msg.overridesChanged, alsoShowHidden); + } + + if ("shimsChanged" in msg) { + updateShimTables(msg.shimsChanged, alsoShowHidden); + } + + 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, shims } = availablePatches; + const alsoShowHidden = location.hash === "#all"; + redrawTable($("#overrides"), overrides, alsoShowHidden); + redrawTable($("#interventions"), interventions, alsoShowHidden); + updateShimTables(shims, alsoShowHidden); +} + +function clearTableAndAddMessage(table, msgId) { + table.querySelectorAll("tr").forEach(tr => { + tr.remove(); + }); + + const tr = document.createElement("tr"); + tr.className = "message"; + tr.id = msgId; + + const td = document.createElement("td"); + td.setAttribute("colspan", "3"); + document.l10n.setAttributes(td, msgId); + tr.appendChild(td); + + table.appendChild(tr); +} + +function hideMessagesOnTable(table) { + table.querySelectorAll("tr.message").forEach(tr => { + tr.remove(); + }); +} + +function updateShimTables(shimsChanged, alsoShowHidden) { + const tables = document.querySelectorAll("table.shims"); + if (!tables.length) { + return; + } + + for (const { bug, disabledReason, hidden, id, name, type } of shimsChanged) { + // if any shim is disabled by global pref, all of them are. just show the + // "disabled in about:config" message on each shim table in that case. + if (disabledReason === "globalPref") { + for (const table of tables) { + clearTableAndAddMessage(table, "text-disabled-in-about-config"); + } + return; + } + + // otherwise, find which table the shim belongs in. if there is none, + // ignore the shim (we're not showing it on the UI for whatever reason). + const table = document.querySelector(`table.shims#${type}`); + if (!table) { + continue; + } + + // similarly, skip shims hidden from the UI (only for testing, etc). + if (!alsoShowHidden && hidden) { + continue; + } + + // also, hide the shim if it is disabled because it is not meant for this + // platform, release (etc) rather than being disabled by pref/about:compat + const notApplicable = + disabledReason && + disabledReason !== "pref" && + disabledReason !== "session"; + if (!alsoShowHidden && notApplicable) { + continue; + } + + // create an updated table-row for the shim + const tr = document.createElement("tr"); + tr.setAttribute("data-id", id); + + let td = document.createElement("td"); + td.innerText = name; + tr.appendChild(td); + + td = document.createElement("td"); + const a = document.createElement("a"); + 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, + disabledReason ? "label-enable" : "label-disable" + ); + td.appendChild(button); + + // is it already in the table? + const row = table.querySelector(`tr[data-id="${id}"]`); + if (row) { + row.replaceWith(tr); + } else { + table.appendChild(tr); + } + } + + for (const table of tables) { + if (!table.querySelector("tr:not(.message)")) { + // no shims? then add a message that none are available for this platform/config + clearTableAndAddMessage(table, `text-no-${table.id}`); + } else { + // otherwise hide any such message, since we have shims on the list + hideMessagesOnTable(table); + } + } +} + +function redrawTable(table, data, alsoShowHidden) { + 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 = `text-no-${table.id}`; + } + + 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 && !alsoShowHidden) { + 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..0f2e7c4ad4 --- /dev/null +++ b/browser/extensions/webcompat/about-compat/aboutPage.js @@ -0,0 +1,46 @@ +/* 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 */ + +const Services = + globalThis.Services || + ChromeUtils.import("resource://gre/modules/Services.jsm").Services; + +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..13cb4fd0bf --- /dev/null +++ b/browser/extensions/webcompat/about-compat/aboutPageProcessScript.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/. */ + +/* eslint-env mozilla/process-script */ + +"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.importESModule( + "resource://gre/modules/ComponentUtils.sys.mjs" + ); + + 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 + ); +} |