diff --git a/browser/components/nsBrowserGlue.js b/browser/components/nsBrowserGlue.js index b53536a0e5..a9d95bb0cf 100644 --- a/browser/components/nsBrowserGlue.js +++ b/browser/components/nsBrowserGlue.js @@ -9,6 +9,8 @@ const Cu = Components.utils; const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const PREF_INTERNAL_USERSCRIPTS_ENABLED = "browser.internal-userscripts.enabled"; + Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/AppConstants.jsm"); @@ -185,6 +187,7 @@ BrowserGlue.prototype = { break; case "final-ui-startup": this._finalUIStartup(); + this._syncInternalUserScripts(); break; case "browser-delayed-startup-finished": this._onFirstWindowLoaded(subject); @@ -357,6 +360,11 @@ BrowserGlue.prototype = { break; case "autocomplete-did-enter-text": break; + case "nsPref:changed": + if (data == PREF_INTERNAL_USERSCRIPTS_ENABLED) { + this._syncInternalUserScripts(); + } + break; case "test-initialize-sanitizer": this._sanitizer.onStartup(); break; @@ -398,6 +406,7 @@ BrowserGlue.prototype = { os.addObserver(this, "flash-plugin-hang", false); os.addObserver(this, "xpi-signature-changed", false); os.addObserver(this, "autocomplete-did-enter-text", false); + Services.prefs.addObserver(PREF_INTERNAL_USERSCRIPTS_ENABLED, this); if (AppConstants.NIGHTLY_BUILD) { os.addObserver(this, AddonWatcher.TOPIC_SLOW_ADDON_DETECTED, false); @@ -448,6 +457,7 @@ BrowserGlue.prototype = { os.removeObserver(this, "flash-plugin-hang"); os.removeObserver(this, "xpi-signature-changed"); os.removeObserver(this, "autocomplete-did-enter-text"); + Services.prefs.removeObserver(PREF_INTERNAL_USERSCRIPTS_ENABLED, this); }, _onAppDefaults: function() { @@ -816,6 +826,15 @@ BrowserGlue.prototype = { nb.PRIORITY_WARNING_MEDIUM, buttons); }, + _syncInternalUserScripts: function () { + let enabled = true; + try { + enabled = Services.prefs.getBoolPref(PREF_INTERNAL_USERSCRIPTS_ENABLED); + } catch (ex) {} + // Internal userscripts service checks this pref on startup. + Services.prefs.setBoolPref(PREF_INTERNAL_USERSCRIPTS_ENABLED, enabled); + }, + // the first browser window has finished initializing _onFirstWindowLoaded: function(aWindow) { // Initialize PdfJs when running in-process and remote. This only diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in index a01855b6b5..c271a445e7 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -357,6 +357,8 @@ @RESPATH@/browser/components/nsBrowserGlue.js @RESPATH@/browser/components/nsSetDefaultBrowser.manifest @RESPATH@/browser/components/nsSetDefaultBrowser.js +@RESPATH@/browser/components/internaluserscripts.js +@RESPATH@/browser/components/internaluserscripts.manifest @RESPATH@/browser/components/devtools-startup.manifest @RESPATH@/browser/components/devtools-startup.js @RESPATH@/browser/components/webideCli.js @@ -585,6 +587,9 @@ @RESPATH@/browser/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/chrome.manifest @RESPATH@/browser/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/icon.png @RESPATH@/browser/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/install.rdf +@RESPATH@/browser/chrome/internaluserscripts.manifest +@RESPATH@/browser/chrome/internaluserscripts/* +@RESPATH@/browser/internal-userscripts/* @RESPATH@/chrome/toolkit@JAREXT@ @RESPATH@/chrome/toolkit.manifest @RESPATH@/chrome/recording.manifest diff --git a/browser/internaluserscripts/README.md b/browser/internaluserscripts/README.md new file mode 100644 index 0000000000..1831a58f2d --- /dev/null +++ b/browser/internaluserscripts/README.md @@ -0,0 +1,28 @@ +# Internal Userscripts (Polyfills Only) + +This directory contains a minimal, built-in userscript loader used to ship polyfill user scripts with Dactyloidae (forked from Basilisk's implementation of such). This originally started as a fork of GreaseMonkey, however at this point very little of GreaseMonkey remains. + +## How it works +- The XPCOM component `@internaluserscripts.mozdev.org/service;1` observes document creation and, when `browser.internal-userscripts.enabled` is true, injects bundled polyfill scripts into each page. +- Bundled scripts live in `basilisk/internaluserscripts/bundled-scripts/` and are packaged into the app. They are loaded in the page principal and can override missing APIs. +- The only default pref is `browser.internal-userscripts.enabled` (default: true). Toggle to disable all injection. + +## Bundled polyfills +- `elementfrompoint-finite-polyfill.user.js`: wraps `Document.elementFromPoint()` and `Document.elementsFromPoint()` to return `null`/`[]` when coordinates are not finite, avoiding page-breaking `TypeError`s from non-finite inputs. It exposes `window.__internalUserscriptsElementFromPointFinitePolyfill = true` for verification. +- `getanimations-polyfill.user.js`: minimal `getAnimations()` shim for `Document`, `Element`, and `CSSPseudoElement` that returns an empty array when native support is unavailable. It exposes `window.__internalUserscriptsGetAnimationsPolyfill = true` for verification. +- `imagedecode-polyfill.user.js`: best-effort `HTMLImageElement.decode()` shim that resolves on load and rejects on error. It exposes `window.__internalUserscriptsImageDecodePolyfill = true` for verification. +- `intl-displaynames-polyfill.user.js`: minimal Intl.DisplayNames shim that validates options and returns the input code when display data is unavailable. It exposes `window.__internalUserscriptsIntlDisplayNamesPolyfill = true` for verification. +- `intl-listformat-polyfill.user.js`: minimal Intl.ListFormat shim supporting `format()` and `formatToParts()` with simple fallback separators. It exposes `window.__internalUserscriptsIntlListFormatPolyfill = true` for verification. +- `intl-relativetimeformat-formattoparts-polyfill.user.js`: adds `formatToParts()` to native Intl.RelativeTimeFormat implementations that lack it. It exposes `window.__internalUserscriptsIntlRelativeTimeFormatFormatToPartsPolyfill = true` for verification. +- `intl-segmenter-polyfill.user.js`: minimal Intl.Segmenter shim supporting `segment()`, `resolvedOptions()`, and `supportedLocalesOf()` with best-effort grapheme/word/sentence segmentation. It exposes `window.__internalUserscriptsIntlSegmenterPolyfill = true` for verification. +- `readablestream-pipethrough-polyfill.user.js`: best-effort ReadableStream `pipeThrough` implementation backed by `pipeTo` or reader/writer pumping. It exposes `window.__internalUserscriptsReadableStreamPipeThroughPolyfill = true` for verification. +- `readablestream-pipeto-polyfill.user.js`: best-effort ReadableStream `pipeTo` implementation using reader/writer pumping. It exposes `window.__internalUserscriptsReadableStreamPipeToPolyfill = true` for verification. +- `textencoderstream-polyfill.user.js`: best-effort TextEncoderStream implementation backed by TransformStream. It exposes `window.__internalUserscriptsTextEncoderStreamPolyfill = true` for verification. +- `textdecoderstream-polyfill.user.js`: best-effort TextDecoderStream implementation backed by TransformStream. It exposes `window.__internalUserscriptsTextDecoderStreamPolyfill = true` for verification. +- `transformstream-polyfill.user.js`: minimal TransformStream polyfill backed by ReadableStream with a lightweight WritableStream shim. It exposes `window.__internalUserscriptsTransformStreamPolyfill = true` for verification. +- `webauthn-microsoft-shim.user.js`: Microsoft-domain WebAuthn capability shim that reports WebAuthn as unsupported by providing rejecting `navigator.credentials.create/get` stubs (when `navigator.credentials` is missing) and a `PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()` fallback that resolves `false`. It exposes `window.__internalUserscriptsWebAuthnMicrosoftShim = true` for verification. + +## Adding new polyfills +1. Drop a `*.user.js` file into `bundled-scripts/` with the appropriate header. +2. List it in `moz.build` under `FINAL_TARGET_FILES['internal-userscripts']`. +3. The loader will inject it automatically when enabled. diff --git a/browser/internaluserscripts/bundled-scripts/elementfrompoint-finite-polyfill.user.js b/browser/internaluserscripts/bundled-scripts/elementfrompoint-finite-polyfill.user.js new file mode 100644 index 0000000000..228e149bea --- /dev/null +++ b/browser/internaluserscripts/bundled-scripts/elementfrompoint-finite-polyfill.user.js @@ -0,0 +1,93 @@ +// 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/. +// ==UserScript== +// @name elementFromPoint finite-args shim +// @namespace internal-userscripts +// @description Prevents throws when Document.elementFromPoint / elementsFromPoint receives non-finite coordinates. +// @match *://*/* +// @grant none +// @run-at document-start +// ==/UserScript== +(function () { + "use strict"; + + var global = typeof window !== "undefined" ? window : null; + if (!global || !global.Document || !global.Document.prototype) { + return; + } + + var patched = false; + + function toFiniteNumber(value) { + var number = Number(value); + if (number !== number || number === Infinity || number === -Infinity) { + return null; + } + return number; + } + + function fallbackValue(methodName) { + return methodName === "elementsFromPoint" ? [] : null; + } + + function wrapMethod(proto, methodName) { + var nativeMethod = proto[methodName]; + if (typeof nativeMethod !== "function") { + return; + } + if (nativeMethod.__internalUserscriptsFinitePointShim) { + return; + } + + function wrappedMethod(x, y) { + var finiteX = toFiniteNumber(x); + var finiteY = toFiniteNumber(y); + if (finiteX === null || finiteY === null) { + return fallbackValue(methodName); + } + + try { + return nativeMethod.call(this, finiteX, finiteY); + } catch (e) { + var message = e && e.message ? String(e.message) : ""; + if (message.indexOf("not a finite floating-point value") !== -1 || + message.indexOf("is not finite") !== -1) { + return fallbackValue(methodName); + } + throw e; + } + } + + try { + Object.defineProperty(wrappedMethod, "__internalUserscriptsFinitePointShim", { + value: true + }); + } catch (e) {} + + try { + Object.defineProperty(proto, methodName, { + value: wrappedMethod, + writable: true, + configurable: true + }); + } catch (e) { + try { + proto[methodName] = wrappedMethod; + } catch (ignore) {} + } + + if (proto[methodName] === wrappedMethod) { + patched = true; + } + } + + wrapMethod(global.Document.prototype, "elementFromPoint"); + wrapMethod(global.Document.prototype, "elementsFromPoint"); + + if (patched) { + try { + global.__internalUserscriptsElementFromPointFinitePolyfill = true; + } catch (e) {} + } +})(); diff --git a/browser/internaluserscripts/bundled-scripts/getanimations-polyfill.user.js b/browser/internaluserscripts/bundled-scripts/getanimations-polyfill.user.js new file mode 100644 index 0000000000..8be664dbf2 --- /dev/null +++ b/browser/internaluserscripts/bundled-scripts/getanimations-polyfill.user.js @@ -0,0 +1,52 @@ +// 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/. +// ==UserScript== +// @name getAnimations Polyfill (minimal) +// @namespace internal-userscripts +// @description Adds minimal Document/Element getAnimations() methods that return an empty array when unsupported. +// @match *://*/* +// @grant none +// @run-at document-start +// ==/UserScript== +(function () { + "use strict"; + + var global = typeof window !== "undefined" ? window : null; + if (!global) { + return; + } + + var patched = false; + + function getAnimations() { + return []; + } + + function defineGetAnimations(proto) { + if (!proto || typeof proto.getAnimations === "function") { + return; + } + + try { + Object.defineProperty(proto, "getAnimations", { + value: getAnimations, + writable: true, + configurable: true + }); + } catch (e) { + proto.getAnimations = getAnimations; + } + patched = true; + } + + defineGetAnimations(global.Document && global.Document.prototype); + defineGetAnimations(global.Element && global.Element.prototype); + defineGetAnimations(global.CSSPseudoElement && global.CSSPseudoElement.prototype); + + if (patched) { + try { + global.__internalUserscriptsGetAnimationsPolyfill = true; + } catch (e) {} + } +})(); diff --git a/browser/internaluserscripts/bundled-scripts/imagedecode-polyfill.user.js b/browser/internaluserscripts/bundled-scripts/imagedecode-polyfill.user.js new file mode 100644 index 0000000000..74505f27c9 --- /dev/null +++ b/browser/internaluserscripts/bundled-scripts/imagedecode-polyfill.user.js @@ -0,0 +1,111 @@ +// 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/. +// ==UserScript== +// @name HTMLImageElement.decode Polyfill (best-effort) +// @namespace internal-userscripts +// @description Adds a Promise-based decode() that resolves on load and rejects on error. +// @match *://*/* +// @grant none +// @run-at document-start +// ==/UserScript== +(function () { + "use strict"; + + var global = typeof window !== "undefined" ? window : null; + if (!global || !global.HTMLImageElement) { + return; + } + + var proto = global.HTMLImageElement.prototype; + if (typeof proto.decode === "function") { + return; + } + + var PromiseCtor = global.Promise; + if (typeof PromiseCtor !== "function") { + return; + } + + function rejectPromise(error) { + return new PromiseCtor(function (resolve, reject) { + reject(error); + }); + } + + function hasLoadedImage(img) { + return img.complete && (img.naturalWidth > 0 || img.naturalHeight > 0); + } + + function isBrokenImage(img) { + return img.complete && img.naturalWidth === 0 && img.naturalHeight === 0; + } + + function decode() { + var img = this; + if (!(img instanceof global.HTMLImageElement)) { + return rejectPromise(new TypeError("decode can only be used on an HTMLImageElement")); + } + + if (hasLoadedImage(img)) { + return PromiseCtor.resolve(); + } + if (isBrokenImage(img)) { + return rejectPromise(new Error("Image decode failed")); + } + + var src = ""; + try { + src = img.currentSrc || img.src || ""; + } catch (e) {} + if (!src) { + return rejectPromise(new Error("Image has no src")); + } + + return new PromiseCtor(function (resolve, reject) { + var settled = false; + + function cleanup() { + if (settled) { + return; + } + settled = true; + img.removeEventListener("load", onLoad, false); + img.removeEventListener("error", onError, false); + } + + function onLoad() { + cleanup(); + resolve(); + } + + function onError() { + cleanup(); + reject(new Error("Image decode failed")); + } + + img.addEventListener("load", onLoad, false); + img.addEventListener("error", onError, false); + + if (hasLoadedImage(img)) { + cleanup(); + resolve(); + } else if (isBrokenImage(img)) { + cleanup(); + reject(new Error("Image decode failed")); + } + }); + } + + try { + Object.defineProperty(proto, "decode", { + value: decode, + writable: true, + configurable: true + }); + } catch (e) { + proto.decode = decode; + } + + try { global.__internalUserscriptsImageDecodePolyfill = true; } catch (e) {} +})(); diff --git a/browser/internaluserscripts/bundled-scripts/intl-displaynames-polyfill.user.js b/browser/internaluserscripts/bundled-scripts/intl-displaynames-polyfill.user.js new file mode 100644 index 0000000000..5729fcbc11 --- /dev/null +++ b/browser/internaluserscripts/bundled-scripts/intl-displaynames-polyfill.user.js @@ -0,0 +1,104 @@ +// 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/. +// ==UserScript== +// @name Intl.DisplayNames Polyfill (minimal) +// @namespace internal-userscripts +// @description Provides a minimal Intl.DisplayNames implementation for environments without native support. +// @match *://*/* +// @grant none +// @run-at document-start +// ==/UserScript== +(function () { + "use strict"; + + if (typeof Intl !== "object" || typeof Intl.DisplayNames === "function") { + return; + } + + var STYLE_VALUES = { long: true, short: true, narrow: true }; + var TYPE_VALUES = { language: true, region: true, script: true, currency: true }; + var FALLBACK_VALUES = { code: true, none: true }; + + function canonicalizeLocales(locales) { + if (typeof Intl.getCanonicalLocales === "function") { + return Intl.getCanonicalLocales(locales); + } + if (locales == null) { + return []; + } + if (Array.isArray(locales)) { + return locales.map(function (loc) { return String(loc); }); + } + return [String(locales)]; + } + + function defaultLocale() { + try { + if (typeof Intl.DateTimeFormat === "function") { + return Intl.DateTimeFormat().resolvedOptions().locale; + } + } catch (e) {} + if (typeof navigator !== "undefined" && navigator.language) { + return String(navigator.language); + } + return "en-US"; + } + + function DisplayNames(locales, options) { + if (!(this instanceof DisplayNames)) { + throw new TypeError("Intl.DisplayNames must be called with new"); + } + options = options || {}; + + var type = options.type; + if (type == null) { + throw new TypeError("Intl.DisplayNames requires a type option"); + } + type = String(type); + if (!TYPE_VALUES[type]) { + throw new RangeError("Invalid type"); + } + + var style = options.style == null ? "long" : String(options.style); + if (!STYLE_VALUES[style]) { + throw new RangeError("Invalid style"); + } + + var fallback = options.fallback == null ? "code" : String(options.fallback); + if (!FALLBACK_VALUES[fallback]) { + throw new RangeError("Invalid fallback"); + } + + var localeList = canonicalizeLocales(locales); + this._locale = localeList.length ? localeList[0] : defaultLocale(); + this._style = style; + this._type = type; + this._fallback = fallback; + } + + DisplayNames.supportedLocalesOf = function (locales) { + return canonicalizeLocales(locales); + }; + + DisplayNames.prototype.resolvedOptions = function () { + return { + locale: this._locale, + style: this._style, + type: this._type, + fallback: this._fallback, + }; + }; + + DisplayNames.prototype.of = function (code) { + if (this._fallback === "none") { + return undefined; + } + return String(code); + }; + + Intl.DisplayNames = DisplayNames; + try { + window.__internalUserscriptsIntlDisplayNamesPolyfill = true; + } catch (e) {} +})(); diff --git a/browser/internaluserscripts/bundled-scripts/intl-listformat-polyfill.user.js b/browser/internaluserscripts/bundled-scripts/intl-listformat-polyfill.user.js new file mode 100644 index 0000000000..3f1232f04b --- /dev/null +++ b/browser/internaluserscripts/bundled-scripts/intl-listformat-polyfill.user.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/. +// ==UserScript== +// @name Intl.ListFormat Polyfill (minimal) +// @namespace internal-userscripts +// @description Provides a minimal Intl.ListFormat implementation for environments without native support. +// @match *://*/* +// @grant none +// @run-at document-start +// ==/UserScript== +(function () { + "use strict"; + + if (typeof Intl !== "object" || typeof Intl.ListFormat === "function") { + return; + } + + var TYPE_VALUES = { conjunction: true, disjunction: true, unit: true }; + var STYLE_VALUES = { long: true, short: true, narrow: true }; + var LOCALE_MATCHER_VALUES = { lookup: true, "best fit": true }; + var PATTERNS = { + conjunction: { + long: { pair: " and ", middle: ", ", end: ", and " }, + short: { pair: " & ", middle: ", ", end: ", & " }, + narrow: { pair: ", ", middle: ", ", end: ", " }, + }, + disjunction: { + long: { pair: " or ", middle: ", ", end: ", or " }, + short: { pair: " or ", middle: ", ", end: ", or " }, + narrow: { pair: " or ", middle: ", ", end: ", or " }, + }, + unit: { + long: { pair: ", ", middle: ", ", end: ", " }, + short: { pair: ", ", middle: ", ", end: ", " }, + narrow: { pair: " ", middle: " ", end: " " }, + }, + }; + + function canonicalizeLocales(locales) { + if (typeof Intl.getCanonicalLocales === "function") { + return Intl.getCanonicalLocales(locales); + } + if (locales == null) { + return []; + } + if (Array.isArray(locales)) { + return locales.map(function (locale) { + return String(locale); + }); + } + return [String(locales)]; + } + + function defaultLocale() { + try { + if (typeof Intl.DateTimeFormat === "function") { + return Intl.DateTimeFormat().resolvedOptions().locale; + } + } catch (e) {} + if (typeof navigator !== "undefined" && navigator.language) { + return String(navigator.language); + } + return "en-US"; + } + + function toStringValue(value) { + if (typeof value === "symbol") { + throw new TypeError("Cannot convert a Symbol value to a string"); + } + return String(value); + } + + function toStringList(list) { + if (list == null) { + throw new TypeError("List argument is required"); + } + + var result = []; + if (typeof Symbol !== "undefined" && list[Symbol.iterator] != null) { + var iterator = list[Symbol.iterator](); + if (!iterator || typeof iterator.next !== "function") { + throw new TypeError("List argument must be iterable"); + } + while (true) { + var step = iterator.next(); + if (step.done) { + break; + } + result.push(toStringValue(step.value)); + } + return result; + } + + // Fallback for very old environments without Symbol.iterator. + if (Array.isArray(list)) { + for (var i = 0; i < list.length; i++) { + result.push(toStringValue(list[i])); + } + return result; + } + + throw new TypeError("List argument must be iterable"); + } + + function requireListFormatInstance(value) { + if ( + value == null || + (typeof value !== "object" && typeof value !== "function") || + !TYPE_VALUES[value._type] || + !STYLE_VALUES[value._style] + ) { + throw new TypeError( + "Method Intl.ListFormat.prototype called on incompatible receiver", + ); + } + return value; + } + + function buildParts(items, pattern) { + var parts = []; + var i = 0; + var lastIndex = items.length - 1; + + if (items.length === 0) { + return parts; + } + if (items.length === 1) { + parts.push({ type: "element", value: items[0] }); + return parts; + } + if (items.length === 2) { + parts.push({ type: "element", value: items[0] }); + parts.push({ type: "literal", value: pattern.pair }); + parts.push({ type: "element", value: items[1] }); + return parts; + } + + parts.push({ type: "element", value: items[0] }); + for (i = 1; i < lastIndex; i++) { + parts.push({ type: "literal", value: pattern.middle }); + parts.push({ type: "element", value: items[i] }); + } + parts.push({ type: "literal", value: pattern.end }); + parts.push({ type: "element", value: items[lastIndex] }); + return parts; + } + + function ListFormat(locales, options) { + if (!(this instanceof ListFormat)) { + throw new TypeError("Intl.ListFormat must be called with new"); + } + + options = options || {}; + var type = options.type == null ? "conjunction" : String(options.type); + var style = options.style == null ? "long" : String(options.style); + var localeMatcher = + options.localeMatcher == null + ? "best fit" + : String(options.localeMatcher); + + if (!TYPE_VALUES[type]) { + throw new RangeError("Invalid type"); + } + if (!STYLE_VALUES[style]) { + throw new RangeError("Invalid style"); + } + if (!LOCALE_MATCHER_VALUES[localeMatcher]) { + throw new RangeError("Invalid localeMatcher"); + } + + var localeList = canonicalizeLocales(locales); + this._locale = localeList.length ? localeList[0] : defaultLocale(); + this._type = type; + this._style = style; + } + + ListFormat.supportedLocalesOf = function (locales, options) { + if (options != null && typeof options === "object") { + var localeMatcher = options.localeMatcher; + if ( + localeMatcher != null && + !LOCALE_MATCHER_VALUES[String(localeMatcher)] + ) { + throw new RangeError("Invalid localeMatcher"); + } + } + return canonicalizeLocales(locales); + }; + + ListFormat.prototype.resolvedOptions = function () { + var self = requireListFormatInstance(this); + return { + locale: self._locale, + type: self._type, + style: self._style, + }; + }; + + ListFormat.prototype.formatToParts = function (list) { + var self = requireListFormatInstance(this); + var items = toStringList(list); + var pattern = PATTERNS[self._type][self._style]; + return buildParts(items, pattern); + }; + + ListFormat.prototype._format = function (list) { + var parts = this.formatToParts(list); + var result = ""; + for (var i = 0; i < parts.length; i++) { + result += parts[i].value; + } + return result; + }; + + Object.defineProperty(ListFormat.prototype, "format", { + configurable: true, + get: function () { + var self = requireListFormatInstance(this); + if (!self._boundFormat) { + self._boundFormat = function (list) { + return self._format(list); + }; + } + return self._boundFormat; + }, + }); + + Intl.ListFormat = ListFormat; + try { + window.__internalUserscriptsIntlListFormatPolyfill = true; + } catch (e) {} +})(); diff --git a/browser/internaluserscripts/bundled-scripts/intl-relativetimeformat-formattoparts-polyfill.user.js b/browser/internaluserscripts/bundled-scripts/intl-relativetimeformat-formattoparts-polyfill.user.js new file mode 100644 index 0000000000..0d0acfc30a --- /dev/null +++ b/browser/internaluserscripts/bundled-scripts/intl-relativetimeformat-formattoparts-polyfill.user.js @@ -0,0 +1,85 @@ +// 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/. +// ==UserScript== +// @name Intl.RelativeTimeFormat formatToParts Polyfill +// @namespace internal-userscripts +// @description Adds formatToParts() to native Intl.RelativeTimeFormat implementations that lack it. +// @match *://*/* +// @grant none +// @run-at document-start +// ==/UserScript== +(function () { + "use strict"; + + if ( + typeof Intl !== "object" || + typeof Intl.RelativeTimeFormat !== "function" || + typeof Intl.RelativeTimeFormat.prototype.formatToParts === "function" + ) { + return; + } + + function formatToParts(value, unit) { + var number = Number(value); + var formatted = this.format(number, unit); + var canonicalUnit = String(unit).replace(/s$/, ""); + var resolved = this.resolvedOptions(); + var numberOptions = { + maximumFractionDigits: 3, + }; + + if (resolved.numberingSystem) { + numberOptions.numberingSystem = resolved.numberingSystem; + } + + var numberFormat = new Intl.NumberFormat(resolved.locale, numberOptions); + var numberParts; + if (typeof numberFormat.formatToParts === "function") { + numberParts = numberFormat.formatToParts(Math.abs(number)); + } else { + numberParts = [ + { type: "integer", value: numberFormat.format(Math.abs(number)) }, + ]; + } + + var numberText = numberParts + .map(function (part) { + return part.value; + }) + .join(""); + var numberIndex = formatted.indexOf(numberText); + + // numeric: "auto" may produce a word such as "yesterday" with no number. + if (numberIndex === -1) { + return [{ type: "literal", value: formatted }]; + } + + var parts = []; + if (numberIndex > 0) { + parts.push({ type: "literal", value: formatted.slice(0, numberIndex) }); + } + numberParts.forEach(function (part) { + parts.push({ type: part.type, value: part.value, unit: canonicalUnit }); + }); + if (numberIndex + numberText.length < formatted.length) { + parts.push({ + type: "literal", + value: formatted.slice(numberIndex + numberText.length), + }); + } + return parts; + } + + Object.defineProperty( + Intl.RelativeTimeFormat.prototype, + "formatToParts", + { + configurable: true, + writable: true, + value: formatToParts, + }, + ); + + window.__internalUserscriptsIntlRelativeTimeFormatFormatToPartsPolyfill = true; +})(); diff --git a/browser/internaluserscripts/bundled-scripts/intl-segmenter-polyfill.user.js b/browser/internaluserscripts/bundled-scripts/intl-segmenter-polyfill.user.js new file mode 100644 index 0000000000..2577e66388 --- /dev/null +++ b/browser/internaluserscripts/bundled-scripts/intl-segmenter-polyfill.user.js @@ -0,0 +1,365 @@ +// 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/. +// ==UserScript== +// @name Intl.Segmenter Polyfill (minimal) +// @namespace internal-userscripts +// @description Provides a minimal Intl.Segmenter implementation for environments without native support. +// @match *://*/* +// @grant none +// @run-at document-start +// ==/UserScript== +(function () { + "use strict"; + + var global = + (typeof globalThis !== "undefined" && globalThis) || + (typeof window !== "undefined" && window) || + (typeof self !== "undefined" && self) || + this; + if (typeof global.Intl !== "object" || global.Intl == null) { + global.Intl = {}; + } + if (typeof global.Intl.Segmenter === "function") { + return; + } + + var GRANULARITY_VALUES = { grapheme: true, word: true, sentence: true }; + var LOCALE_MATCHER_VALUES = { lookup: true, "best fit": true }; + var FALLBACK_WORD_LIKE = /^[A-Za-z0-9_]+$/; + var FALLBACK_WORD_SPLIT = /[A-Za-z0-9_]+|[\s]+|[^\s]/g; + var COMBINING_MARK_RANGES = [ + [0x0300, 0x036f], + [0x1ab0, 0x1aff], + [0x1dc0, 0x1dff], + [0x20d0, 0x20ff], + [0xfe20, 0xfe2f], + ]; + + var unicodeGraphemeRegex = null; + var unicodeWordSplit = null; + var unicodeWordLike = null; + try { + unicodeGraphemeRegex = new RegExp( + "(?:\\P{Mark}\\p{Mark}*|\\p{Mark}+)", + "gu", + ); + unicodeWordSplit = new RegExp("[\\p{L}\\p{N}\\p{M}_]+|[\\s]+|[^\\s]", "gu"); + unicodeWordLike = new RegExp("^[\\p{L}\\p{N}\\p{M}_]+$", "u"); + } catch (e) {} + + function canonicalizeLocales(locales) { + if (typeof global.Intl.getCanonicalLocales === "function") { + return global.Intl.getCanonicalLocales(locales); + } + if (locales == null) { + return []; + } + if (Array.isArray(locales)) { + return locales.map(function (locale) { + return String(locale); + }); + } + return [String(locales)]; + } + + function defaultLocale() { + try { + if (typeof global.Intl.DateTimeFormat === "function") { + return global.Intl.DateTimeFormat().resolvedOptions().locale; + } + } catch (e) {} + if (typeof navigator !== "undefined" && navigator.language) { + return String(navigator.language); + } + return "en-US"; + } + + function toStringValue(value) { + if (typeof value === "symbol") { + throw new TypeError("Cannot convert a Symbol value to a string"); + } + return String(value); + } + + function readCodePoint(str, index) { + var first = str.charCodeAt(index); + if (first >= 0xd800 && first <= 0xdbff && index + 1 < str.length) { + var second = str.charCodeAt(index + 1); + if (second >= 0xdc00 && second <= 0xdfff) { + return { + codePoint: ((first - 0xd800) << 10) + (second - 0xdc00) + 0x10000, + length: 2, + }; + } + } + return { codePoint: first, length: 1 }; + } + + function isCombiningMark(codePoint) { + for (var i = 0; i < COMBINING_MARK_RANGES.length; i++) { + var range = COMBINING_MARK_RANGES[i]; + if (codePoint >= range[0] && codePoint <= range[1]) { + return true; + } + } + return false; + } + + function segmentGrapheme(string) { + var segments = []; + var i = 0; + + if (unicodeGraphemeRegex) { + var match; + unicodeGraphemeRegex.lastIndex = 0; + while ((match = unicodeGraphemeRegex.exec(string))) { + segments.push({ + segment: match[0], + index: match.index, + }); + } + return segments; + } + + while (i < string.length) { + var start = i; + var current = readCodePoint(string, i); + i += current.length; + while (i < string.length) { + var next = readCodePoint(string, i); + if (!isCombiningMark(next.codePoint)) { + break; + } + i += next.length; + } + segments.push({ + segment: string.slice(start, i), + index: start, + }); + } + + return segments; + } + + function segmentWord(string) { + var segments = []; + var regex = unicodeWordSplit || FALLBACK_WORD_SPLIT; + var match; + + regex.lastIndex = 0; + while ((match = regex.exec(string))) { + segments.push({ + segment: match[0], + index: match.index, + }); + } + return segments; + } + + function isSentenceTerminal(ch) { + return ch === "." || ch === "!" || ch === "?"; + } + + function isWhitespaceChar(ch) { + return ( + ch === " " || ch === "\t" || ch === "\r" || ch === "\n" || ch === "\f" + ); + } + + function segmentSentence(string) { + var segments = []; + var start = 0; + var i = 0; + + if (!string) { + return segments; + } + + while (i < string.length) { + if (isSentenceTerminal(string.charAt(i))) { + i++; + while (i < string.length && isSentenceTerminal(string.charAt(i))) { + i++; + } + while (i < string.length && isWhitespaceChar(string.charAt(i))) { + i++; + } + segments.push({ + segment: string.slice(start, i), + index: start, + }); + start = i; + } else { + i++; + } + } + + if (start < string.length) { + segments.push({ + segment: string.slice(start), + index: start, + }); + } + + return segments; + } + + function segmentString(string, granularity) { + if (granularity === "word") { + return segmentWord(string); + } + if (granularity === "sentence") { + return segmentSentence(string); + } + return segmentGrapheme(string); + } + + function isWordLikeSegment(segment) { + if (unicodeWordLike) { + return unicodeWordLike.test(segment); + } + return FALLBACK_WORD_LIKE.test(segment); + } + + function requireSegmenter(value) { + if ( + value == null || + (typeof value !== "object" && typeof value !== "function") || + !GRANULARITY_VALUES[value._granularity] + ) { + throw new TypeError( + "Method Intl.Segmenter.prototype called on incompatible receiver", + ); + } + return value; + } + + function SegmentIterator(segments) { + this._segments = segments; + this._index = 0; + } + + SegmentIterator.prototype.next = function () { + if (this._index >= this._segments.length) { + return { value: undefined, done: true }; + } + var value = this._segments[this._index]; + this._index += 1; + return { value: value, done: false }; + }; + + if (typeof Symbol !== "undefined" && Symbol.iterator) { + SegmentIterator.prototype[Symbol.iterator] = function () { + return this; + }; + } + + function Segments(input, segments) { + this._input = input; + this._segments = segments; + } + + Segments.prototype.containing = function (index) { + if (index == null) { + return undefined; + } + var position = Number(index); + if (!isFinite(position)) { + return undefined; + } + position = Math.floor(position); + for (var i = 0; i < this._segments.length; i++) { + var segment = this._segments[i]; + if ( + position >= segment.index && + position < segment.index + segment.segment.length + ) { + return segment; + } + } + return undefined; + }; + + if (typeof Symbol !== "undefined" && Symbol.iterator) { + Segments.prototype[Symbol.iterator] = function () { + return new SegmentIterator(this._segments); + }; + } + + function Segmenter(locales, options) { + if (!(this instanceof Segmenter)) { + throw new TypeError("Intl.Segmenter must be called with new"); + } + + if (options == null) { + options = {}; + } else if (typeof options !== "object") { + options = Object(options); + } + + var granularity = + options.granularity == null ? "grapheme" : String(options.granularity); + if (!GRANULARITY_VALUES[granularity]) { + throw new RangeError("Invalid granularity"); + } + if ( + options.localeMatcher != null && + !LOCALE_MATCHER_VALUES[String(options.localeMatcher)] + ) { + throw new RangeError("Invalid localeMatcher"); + } + + var localeList = canonicalizeLocales(locales); + this._locale = localeList.length ? localeList[0] : defaultLocale(); + this._granularity = granularity; + } + + Segmenter.supportedLocalesOf = function (locales, options) { + if (options != null && typeof options === "object") { + var localeMatcher = options.localeMatcher; + if ( + localeMatcher != null && + !LOCALE_MATCHER_VALUES[String(localeMatcher)] + ) { + throw new RangeError("Invalid localeMatcher"); + } + } + return canonicalizeLocales(locales); + }; + + Segmenter.prototype.resolvedOptions = function () { + var self = requireSegmenter(this); + return { + locale: self._locale, + granularity: self._granularity, + }; + }; + + Segmenter.prototype.segment = function (input) { + var self = requireSegmenter(this); + var string = toStringValue(input); + var entries = segmentString(string, self._granularity); + var segments = []; + + for (var i = 0; i < entries.length; i++) { + var part = entries[i]; + segments.push({ + segment: part.segment, + index: part.index, + input: string, + isWordLike: + self._granularity === "word" + ? isWordLikeSegment(part.segment) + : undefined, + }); + } + + return new Segments(string, segments); + }; + + global.Intl.Segmenter = Segmenter; + try { + global.__internalUserscriptsIntlSegmenterPolyfill = true; + } catch (e) {} +})(); diff --git a/browser/internaluserscripts/bundled-scripts/readablestream-pipethrough-polyfill.user.js b/browser/internaluserscripts/bundled-scripts/readablestream-pipethrough-polyfill.user.js new file mode 100644 index 0000000000..2d569a91a2 --- /dev/null +++ b/browser/internaluserscripts/bundled-scripts/readablestream-pipethrough-polyfill.user.js @@ -0,0 +1,208 @@ +// 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/. +// ==UserScript== +// @name ReadableStream pipeThrough Polyfill (best-effort) +// @namespace internal-userscripts +// @description Adds ReadableStream.prototype.pipeThrough using pipeTo or reader/writer pumping when missing. +// @match *://*/* +// @grant none +// @run-at document-start +// ==/UserScript== +(function () { + "use strict"; + + var global = typeof window !== "undefined" ? window : null; + if (!global || !global.ReadableStream || !global.ReadableStream.prototype) { + return; + } + var existingPipeThrough = global.ReadableStream.prototype.pipeThrough; + if (typeof existingPipeThrough === "function") { + if (typeof global.TransformStream !== "function") { + return; + } + try { + var probeStream = new global.ReadableStream({ + start: function (controller) { + controller.close(); + } + }); + probeStream.pipeThrough(new global.TransformStream(), { preventClose: true }); + return; + } catch (e) { + if (String(e).indexOf("not yet implemented") === -1) { + return; + } + } + } + + var PromiseCtor = global.Promise; + if (typeof PromiseCtor !== "function") { + return; + } + + function isObject(value) { + return value !== null && (typeof value === "object" || typeof value === "function"); + } + + function getTransformStream(transform, name) { + var stream = transform[name]; + if (!isObject(stream)) { + throw new TypeError("pipeThrough requires a transform stream with " + name); + } + return stream; + } + + function startPipeWithReader(source, writable, options) { + var reader = source.getReader(); + var writer = writable.getWriter(); + var preventClose = options && options.preventClose; + var preventAbort = options && options.preventAbort; + var preventCancel = options && options.preventCancel; + var signal = options && options.signal; + var abortHandler = null; + + function safeReleaseLock(stream, maybeReader, lockedProp) { + if (!maybeReader) { + return; + } + var isLocked = true; + if (typeof lockedProp === "function") { + try { + var lockedValue = lockedProp(); + if (typeof lockedValue === "boolean") { + isLocked = lockedValue; + } + } catch (e) { + isLocked = true; + } + } + if (!isLocked) { + return; + } + var releaseFn = null; + try { + releaseFn = maybeReader.releaseLock; + } catch (e) { + releaseFn = null; + } + if (typeof releaseFn === "function") { + try { releaseFn.call(maybeReader); } catch (e) {} + } + } + + function releaseLocks() { + safeReleaseLock(source, reader, function () { return source.locked; }); + safeReleaseLock(writable, writer, function () { return writable.locked; }); + reader = null; + writer = null; + } + + function cleanupAbort() { + if (abortHandler && signal && typeof signal.removeEventListener === "function") { + signal.removeEventListener("abort", abortHandler); + } + } + + if (signal) { + abortHandler = function () { + var reason = signal.reason; + if (!preventCancel) { + try { reader.cancel(reason); } catch (e) {} + } + if (!preventAbort) { + try { writer.abort(reason); } catch (e) {} + } + }; + if (signal.aborted) { + abortHandler(); + cleanupAbort(); + releaseLocks(); + return; + } + if (typeof signal.addEventListener === "function") { + signal.addEventListener("abort", abortHandler); + } + } + + function pump() { + var readPromise; + try { + readPromise = reader.read(); + } catch (e) { + return PromiseCtor.reject(e); + } + return readPromise.then(function (result) { + if (result.done) { + if (!preventClose) { + var closePromise; + try { + closePromise = writer.close(); + } catch (e) { + return PromiseCtor.reject(e); + } + return PromiseCtor.resolve(closePromise); + } + return undefined; + } + var writePromise; + try { + writePromise = writer.write(result.value); + } catch (e) { + return PromiseCtor.reject(e); + } + return PromiseCtor.resolve(writePromise).then(pump); + }); + } + + pump().catch(function (err) { + if (!preventCancel) { + try { reader.cancel(err); } catch (e) {} + } + if (!preventAbort) { + try { writer.abort(err); } catch (e) {} + } + }).then(function () { + cleanupAbort(); + releaseLocks(); + }, function () { + cleanupAbort(); + releaseLocks(); + }); + } + + function pipeThrough(transform, options) { + if (!isObject(this)) { + throw new TypeError("ReadableStream.prototype.pipeThrough called on incompatible receiver"); + } + if (!isObject(transform)) { + throw new TypeError("pipeThrough requires a transform stream"); + } + var readable = getTransformStream(transform, "readable"); + var writable = getTransformStream(transform, "writable"); + var source = this; + + if (typeof source.pipeTo === "function") { + var pipePromise = source.pipeTo(writable, options); + if (pipePromise && typeof pipePromise.catch === "function") { + pipePromise.catch(function () {}); + } + } else if (typeof source.getReader === "function" && typeof writable.getWriter === "function") { + startPipeWithReader(source, writable, options); + } else { + throw new TypeError("ReadableStream.prototype.pipeThrough is not supported"); + } + + return readable; + } + + Object.defineProperty(global.ReadableStream.prototype, "pipeThrough", { + value: pipeThrough, + writable: true, + configurable: true + }); + + try { + global.__internalUserscriptsReadableStreamPipeThroughPolyfill = true; + } catch (e) {} +})(); diff --git a/browser/internaluserscripts/bundled-scripts/readablestream-pipeto-polyfill.user.js b/browser/internaluserscripts/bundled-scripts/readablestream-pipeto-polyfill.user.js new file mode 100644 index 0000000000..9c416402d4 --- /dev/null +++ b/browser/internaluserscripts/bundled-scripts/readablestream-pipeto-polyfill.user.js @@ -0,0 +1,221 @@ +// 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/. +// ==UserScript== +// @name ReadableStream pipeTo Polyfill (best-effort) +// @namespace internal-userscripts +// @description Adds ReadableStream.prototype.pipeTo using reader/writer pumping. +// @match *://*/* +// @grant none +// @run-at document-start +// ==/UserScript== +(function () { + "use strict"; + + var global = typeof window !== "undefined" ? window : null; + if (!global || !global.ReadableStream || !global.ReadableStream.prototype) { + return; + } + + var PromiseCtor = global.Promise; + if (typeof PromiseCtor !== "function") { + return; + } + + function isObject(value) { + return value !== null && (typeof value === "object" || typeof value === "function"); + } + + var existingPipeTo = global.ReadableStream.prototype.pipeTo; + if (typeof existingPipeTo === "function") { + if (typeof global.TransformStream !== "function" && + typeof global.WritableStream !== "function") { + return; + } + try { + var testWritable = null; + if (typeof global.TransformStream === "function") { + testWritable = new global.TransformStream().writable; + } else { + testWritable = new global.WritableStream(); + } + var probeStream = new global.ReadableStream({ + start: function (controller) { + controller.close(); + } + }); + probeStream.pipeTo(testWritable, { preventClose: true }); + return; + } catch (e) { + if (String(e).indexOf("not yet implemented") === -1) { + return; + } + } + } + + function pipeTo(writable, options) { + if (!isObject(this)) { + throw new TypeError("ReadableStream.prototype.pipeTo called on incompatible receiver"); + } + if (!isObject(writable) || typeof writable.getWriter !== "function") { + throw new TypeError("pipeTo requires a writable stream"); + } + + var source = this; + var reader = source.getReader(); + var writer = writable.getWriter(); + var preventClose = options && options.preventClose; + var preventAbort = options && options.preventAbort; + var preventCancel = options && options.preventCancel; + var signal = options && options.signal; + var abortHandler = null; + var abortReject = null; + var aborted = false; + var abortReason = undefined; + + function safeReleaseLock(stream, maybeReader, lockedProp) { + if (!maybeReader) { + return; + } + var isLocked = true; + if (typeof lockedProp === "function") { + try { + var lockedValue = lockedProp(); + if (typeof lockedValue === "boolean") { + isLocked = lockedValue; + } + } catch (e) { + isLocked = true; + } + } + if (!isLocked) { + return; + } + var releaseFn = null; + try { + releaseFn = maybeReader.releaseLock; + } catch (e) { + releaseFn = null; + } + if (typeof releaseFn === "function") { + try { releaseFn.call(maybeReader); } catch (e) {} + } + } + + function releaseLocks() { + safeReleaseLock(source, reader, function () { return source.locked; }); + safeReleaseLock(writable, writer, function () { return writable.locked; }); + reader = null; + writer = null; + } + + function cleanupAbort() { + if (abortHandler && signal && typeof signal.removeEventListener === "function") { + signal.removeEventListener("abort", abortHandler); + } + } + + function abortWith(reason) { + if (aborted) { + return; + } + aborted = true; + abortReason = reason; + if (!preventCancel) { + try { reader.cancel(reason); } catch (e) {} + } + if (!preventAbort) { + try { writer.abort(reason); } catch (e) {} + } + if (abortReject) { + abortReject(reason); + } + } + + var abortPromise = null; + if (signal) { + abortPromise = new PromiseCtor(function (_resolve, reject) { + abortReject = reject; + }); + abortHandler = function () { + abortWith(signal.reason); + }; + if (signal.aborted) { + abortWith(signal.reason); + } else if (typeof signal.addEventListener === "function") { + signal.addEventListener("abort", abortHandler); + } + } + + function pump() { + if (aborted) { + return PromiseCtor.reject(abortReason); + } + var readPromise; + try { + readPromise = reader.read(); + } catch (e) { + return PromiseCtor.reject(e); + } + return readPromise.then(function (result) { + if (aborted) { + throw abortReason; + } + if (result.done) { + if (!preventClose) { + return writer.close(); + } + return undefined; + } + var writePromise; + try { + writePromise = writer.write(result.value); + } catch (e) { + return PromiseCtor.reject(e); + } + return PromiseCtor.resolve(writePromise).then(pump); + }); + } + + var pipePromise = pump().catch(function (err) { + if (!preventCancel) { + try { reader.cancel(err); } catch (e) {} + } + if (!preventAbort) { + try { writer.abort(err); } catch (e) {} + } + throw err; + }); + + var finalPromise = abortPromise + ? PromiseCtor.race([pipePromise, abortPromise]) + : pipePromise; + + function settleAndRelease(err) { + return pipePromise.catch(function () {}).then(function () { + cleanupAbort(); + releaseLocks(); + if (err !== undefined) { + throw err; + } + return undefined; + }); + } + + return finalPromise.then(function () { + return settleAndRelease(); + }, function (err) { + return settleAndRelease(err); + }); + } + + Object.defineProperty(global.ReadableStream.prototype, "pipeTo", { + value: pipeTo, + writable: true, + configurable: true + }); + + try { + global.__internalUserscriptsReadableStreamPipeToPolyfill = true; + } catch (e) {} +})(); diff --git a/browser/internaluserscripts/bundled-scripts/textdecoderstream-polyfill.user.js b/browser/internaluserscripts/bundled-scripts/textdecoderstream-polyfill.user.js new file mode 100644 index 0000000000..580d000947 --- /dev/null +++ b/browser/internaluserscripts/bundled-scripts/textdecoderstream-polyfill.user.js @@ -0,0 +1,179 @@ +// 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/. +// ==UserScript== +// @name TextDecoderStream Polyfill (best-effort) +// @namespace internal-userscripts +// @description Minimal TextDecoderStream implementation backed by TransformStream. +// @match *://*/* +// @grant none +// @run-at document-start +// ==/UserScript== +(function () { + "use strict"; + + var global = typeof window !== "undefined" ? window : null; + if (!global) { + return; + } + + var existingCtor = global.TextDecoderStream; + if (typeof existingCtor === "function") { + try { + var probe = new existingCtor(); + if (probe && probe.readable && probe.writable) { + return; + } + } catch (e) { + if (String(e).indexOf("not yet implemented") === -1) { + return; + } + } + } + + if (typeof global.TransformStream !== "function") { + return; + } + if (typeof global.TextDecoder !== "function") { + return; + } + + function isObject(value) { + return value !== null && (typeof value === "object" || typeof value === "function"); + } + + function streamBrandCheckException(name) { + return new TypeError("TextDecoderStream.prototype." + name + + " can only be used on a TextDecoderStream"); + } + + function isTextDecoderStream(value) { + return isObject(value) && value._isTextDecoderStream === true; + } + + function normalizeOptions(options) { + var normalized = { + fatal: false, + ignoreBOM: false + }; + + if (!isObject(options)) { + return normalized; + } + + normalized.fatal = !!options.fatal; + normalized.ignoreBOM = !!options.ignoreBOM; + return normalized; + } + + function PolyfillTextDecoderStream(label, options) { + if (!(this instanceof PolyfillTextDecoderStream)) { + throw new TypeError("TextDecoderStream must be constructed with 'new'"); + } + + var normalizedOptions = normalizeOptions(options); + var effectiveLabel = label === undefined ? "utf-8" : label; + + var decoder; + try { + decoder = new global.TextDecoder(effectiveLabel, { + fatal: normalizedOptions.fatal, + ignoreBOM: normalizedOptions.ignoreBOM + }); + } catch (e) { + decoder = new global.TextDecoder(effectiveLabel, { + fatal: normalizedOptions.fatal + }); + } + + this._decoder = decoder; + this._encoding = typeof decoder.encoding === "string" + ? decoder.encoding + : String(effectiveLabel).toLowerCase(); + this._fatal = typeof decoder.fatal === "boolean" + ? decoder.fatal + : normalizedOptions.fatal; + this._ignoreBOM = typeof decoder.ignoreBOM === "boolean" + ? decoder.ignoreBOM + : normalizedOptions.ignoreBOM; + + this._transform = new global.TransformStream({ + transform: function (chunk, controller) { + var output = decoder.decode(chunk, { stream: true }); + if (output.length > 0) { + controller.enqueue(output); + } + }, + flush: function (controller) { + var output = decoder.decode(); + if (output.length > 0) { + controller.enqueue(output); + } + } + }); + + this._readable = this._transform.readable; + this._writable = this._transform.writable; + this._isTextDecoderStream = true; + } + + Object.defineProperty(PolyfillTextDecoderStream.prototype, "encoding", { + get: function () { + if (!isTextDecoderStream(this)) { + throw streamBrandCheckException("encoding"); + } + return this._encoding; + }, + enumerable: true, + configurable: true + }); + + Object.defineProperty(PolyfillTextDecoderStream.prototype, "fatal", { + get: function () { + if (!isTextDecoderStream(this)) { + throw streamBrandCheckException("fatal"); + } + return this._fatal; + }, + enumerable: true, + configurable: true + }); + + Object.defineProperty(PolyfillTextDecoderStream.prototype, "ignoreBOM", { + get: function () { + if (!isTextDecoderStream(this)) { + throw streamBrandCheckException("ignoreBOM"); + } + return this._ignoreBOM; + }, + enumerable: true, + configurable: true + }); + + Object.defineProperty(PolyfillTextDecoderStream.prototype, "readable", { + get: function () { + if (!isTextDecoderStream(this)) { + throw streamBrandCheckException("readable"); + } + return this._readable; + }, + enumerable: true, + configurable: true + }); + + Object.defineProperty(PolyfillTextDecoderStream.prototype, "writable", { + get: function () { + if (!isTextDecoderStream(this)) { + throw streamBrandCheckException("writable"); + } + return this._writable; + }, + enumerable: true, + configurable: true + }); + + global.TextDecoderStream = PolyfillTextDecoderStream; + try { + global.__internalUserscriptsTextDecoderStreamPolyfill = true; + } catch (e) {} +})(); diff --git a/browser/internaluserscripts/bundled-scripts/textencoderstream-polyfill.user.js b/browser/internaluserscripts/bundled-scripts/textencoderstream-polyfill.user.js new file mode 100644 index 0000000000..e28aef6e23 --- /dev/null +++ b/browser/internaluserscripts/bundled-scripts/textencoderstream-polyfill.user.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/. +// ==UserScript== +// @name TextEncoderStream Polyfill (best-effort) +// @namespace internal-userscripts +// @description Minimal TextEncoderStream implementation backed by TransformStream. +// @match *://*/* +// @grant none +// @run-at document-start +// ==/UserScript== +(function () { + "use strict"; + + var global = typeof window !== "undefined" ? window : null; + if (!global) { + return; + } + + var existingCtor = global.TextEncoderStream; + if (typeof existingCtor === "function") { + try { + var probe = new existingCtor(); + if (probe && probe.readable && probe.writable) { + return; + } + } catch (e) { + if (String(e).indexOf("not yet implemented") === -1) { + return; + } + } + } + + if (typeof global.TransformStream !== "function") { + return; + } + if (typeof global.TextEncoder !== "function") { + return; + } + + function isObject(value) { + return value !== null && (typeof value === "object" || typeof value === "function"); + } + + function streamBrandCheckException(name) { + return new TypeError("TextEncoderStream.prototype." + name + + " can only be used on a TextEncoderStream"); + } + + function isTextEncoderStream(value) { + return isObject(value) && value._isTextEncoderStream === true; + } + + function PolyfillTextEncoderStream() { + if (!(this instanceof PolyfillTextEncoderStream)) { + throw new TypeError("TextEncoderStream must be constructed with 'new'"); + } + + var encoder = new global.TextEncoder(); + this._transform = new global.TransformStream({ + transform: function (chunk, controller) { + controller.enqueue(encoder.encode(String(chunk))); + } + }); + this._readable = this._transform.readable; + this._writable = this._transform.writable; + this._isTextEncoderStream = true; + } + + Object.defineProperty(PolyfillTextEncoderStream.prototype, "readable", { + get: function () { + if (!isTextEncoderStream(this)) { + throw streamBrandCheckException("readable"); + } + return this._readable; + }, + enumerable: true, + configurable: true + }); + + Object.defineProperty(PolyfillTextEncoderStream.prototype, "writable", { + get: function () { + if (!isTextEncoderStream(this)) { + throw streamBrandCheckException("writable"); + } + return this._writable; + }, + enumerable: true, + configurable: true + }); + + global.TextEncoderStream = PolyfillTextEncoderStream; + try { + global.__internalUserscriptsTextEncoderStreamPolyfill = true; + } catch (e) {} +})(); diff --git a/browser/internaluserscripts/bundled-scripts/transformstream-polyfill.user.js b/browser/internaluserscripts/bundled-scripts/transformstream-polyfill.user.js new file mode 100644 index 0000000000..2b820c4abd --- /dev/null +++ b/browser/internaluserscripts/bundled-scripts/transformstream-polyfill.user.js @@ -0,0 +1,430 @@ +// 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/. +// ==UserScript== +// @name TransformStream Polyfill (best-effort) +// @namespace internal-userscripts +// @description Minimal TransformStream implementation backed by ReadableStream and a lightweight WritableStream shim. Not a full Streams spec implementation. +// @match *://*/* +// @grant none +// @run-at document-start +// ==/UserScript== +(function () { + "use strict"; + + var global = typeof window !== "undefined" ? window : null; + if (!global || typeof global.TransformStream === "function") { + return; + } + + var ReadableStreamCtor = global.ReadableStream; + var PromiseCtor = global.Promise; + if (typeof PromiseCtor !== "function") { + return; + } + + function isObject(value) { + return value !== null && (typeof value === "object" || typeof value === "function"); + } + + function streamBrandCheckException(name) { + return new TypeError("TransformStream.prototype." + name + + " can only be used on a TransformStream"); + } + + function PolyfillWritableStream(underlyingSink) { + if (!(this instanceof PolyfillWritableStream)) { + throw new TypeError("WritableStream must be constructed with 'new'"); + } + this._sink = underlyingSink || {}; + this._state = "writable"; + this._storedError = undefined; + this._writer = null; + this._readyPromise = PromiseCtor.resolve(); + + var resolveClosed; + var rejectClosed; + this._closedPromise = new PromiseCtor(function (resolve, reject) { + resolveClosed = resolve; + rejectClosed = reject; + }); + this._closedResolve = resolveClosed; + this._closedReject = rejectClosed; + + if (typeof this._sink.start === "function") { + try { + this._sink.start(this); + } catch (e) { + this._error(e); + } + } + } + + PolyfillWritableStream.prototype.getWriter = function () { + if (this._writer) { + throw new TypeError("WritableStream is locked"); + } + this._writer = new PolyfillWritableStreamDefaultWriter(this); + return this._writer; + }; + + PolyfillWritableStream.prototype._releaseWriter = function (writer) { + if (this._writer === writer) { + this._writer = null; + } + }; + + PolyfillWritableStream.prototype._error = function (reason) { + if (this._state === "closed" || this._state === "errored") { + return; + } + this._state = "errored"; + this._storedError = reason; + if (this._closedReject) { + this._closedReject(reason); + } + }; + + PolyfillWritableStream.prototype._write = function (chunk) { + if (this._state !== "writable") { + return PromiseCtor.reject(new TypeError("WritableStream is not writable")); + } + if (!this._sink || typeof this._sink.write !== "function") { + return PromiseCtor.resolve(); + } + try { + return PromiseCtor.resolve(this._sink.write(chunk)); + } catch (e) { + this._error(e); + return PromiseCtor.reject(e); + } + }; + + PolyfillWritableStream.prototype._close = function () { + if (this._state !== "writable") { + return PromiseCtor.reject(new TypeError("WritableStream is not writable")); + } + this._state = "closing"; + + var closeResult; + if (this._sink && typeof this._sink.close === "function") { + try { + closeResult = this._sink.close(); + } catch (e) { + this._error(e); + return PromiseCtor.reject(e); + } + } + + var self = this; + return PromiseCtor.resolve(closeResult).then(function () { + self._state = "closed"; + if (self._closedResolve) { + self._closedResolve(); + } + }, function (e) { + self._error(e); + throw e; + }); + }; + + PolyfillWritableStream.prototype._abort = function (reason) { + if (this._state === "closed") { + return PromiseCtor.resolve(); + } + this._error(reason); + + if (this._sink && typeof this._sink.abort === "function") { + try { + return PromiseCtor.resolve(this._sink.abort(reason)); + } catch (e) { + return PromiseCtor.reject(e); + } + } + return PromiseCtor.resolve(); + }; + + function PolyfillWritableStreamDefaultWriter(stream) { + this._stream = stream; + } + + PolyfillWritableStreamDefaultWriter.prototype._assertStream = function (name) { + if (!this._stream) { + throw new TypeError("Cannot " + name + " a stream using a released writer"); + } + return this._stream; + }; + + Object.defineProperty(PolyfillWritableStreamDefaultWriter.prototype, "ready", { + get: function () { + return this._assertStream("get ready state for")._readyPromise; + }, + enumerable: true, + configurable: true + }); + + Object.defineProperty(PolyfillWritableStreamDefaultWriter.prototype, "closed", { + get: function () { + return this._assertStream("get closed state for")._closedPromise; + }, + enumerable: true, + configurable: true + }); + + PolyfillWritableStreamDefaultWriter.prototype.write = function (chunk) { + return this._assertStream("write to")._write(chunk); + }; + + PolyfillWritableStreamDefaultWriter.prototype.close = function () { + return this._assertStream("close")._close(); + }; + + PolyfillWritableStreamDefaultWriter.prototype.abort = function (reason) { + return this._assertStream("abort")._abort(reason); + }; + + PolyfillWritableStreamDefaultWriter.prototype.releaseLock = function () { + var stream = this._stream; + if (!stream) { + return; + } + stream._releaseWriter(this); + this._stream = null; + }; + + var WritableStreamCtor = typeof global.WritableStream === "function" + ? global.WritableStream + : PolyfillWritableStream; + + function TransformStreamDefaultController(stream) { + this._stream = stream; + this._readableController = null; + } + + Object.defineProperty(TransformStreamDefaultController.prototype, "desiredSize", { + get: function () { + if (!this._readableController) { + return null; + } + return this._readableController.desiredSize; + }, + enumerable: true, + configurable: true + }); + + TransformStreamDefaultController.prototype.enqueue = function (chunk) { + var controller = this._readableController; + if (!controller) { + throw new TypeError("ReadableStream controller is not available"); + } + controller.enqueue(chunk); + }; + + TransformStreamDefaultController.prototype.error = function (reason) { + this._stream._error(reason); + }; + + TransformStreamDefaultController.prototype.terminate = function () { + this._stream._terminate(); + }; + + function isTransformStream(value) { + return isObject(value) && value._isTransformStream === true; + } + + function PolyfillTransformStream(transformer, writableStrategy, readableStrategy) { + if (!(this instanceof PolyfillTransformStream)) { + throw new TypeError("TransformStream must be constructed with 'new'"); + } + + if (typeof ReadableStreamCtor !== "function") { + throw new TypeError("ReadableStream is not supported in this environment"); + } + + transformer = transformer || {}; + writableStrategy = writableStrategy || {}; + readableStrategy = readableStrategy || {}; + + if (transformer.writableType !== undefined) { + throw new RangeError("Invalid writable type specified"); + } + if (transformer.readableType !== undefined) { + throw new RangeError("Invalid readable type specified"); + } + + this._transformer = transformer; + this._controller = new TransformStreamDefaultController(this); + this._readableClosed = false; + this._errored = false; + this._terminated = false; + this._pendingError = undefined; + this._hasPendingError = false; + this._isTransformStream = true; + + var self = this; + + this._readable = new ReadableStreamCtor({ + start: function (controller) { + self._controller._readableController = controller; + if (self._hasPendingError) { + controller.error(self._pendingError); + return; + } + if (typeof transformer.start === "function") { + return transformer.start(self._controller); + } + }, + pull: function () {}, + cancel: function (reason) { + self._error(reason); + return PromiseCtor.resolve(); + } + }, readableStrategy); + + this._writable = new WritableStreamCtor({ + write: function (chunk) { + return self._transform(chunk); + }, + close: function () { + return self._flush(); + }, + abort: function (reason) { + self._error(reason); + return PromiseCtor.resolve(); + } + }, writableStrategy); + } + + Object.defineProperty(PolyfillTransformStream.prototype, "readable", { + get: function () { + if (!isTransformStream(this)) { + throw streamBrandCheckException("readable"); + } + return this._readable; + }, + enumerable: true, + configurable: true + }); + + Object.defineProperty(PolyfillTransformStream.prototype, "writable", { + get: function () { + if (!isTransformStream(this)) { + throw streamBrandCheckException("writable"); + } + return this._writable; + }, + enumerable: true, + configurable: true + }); + + PolyfillTransformStream.prototype._transform = function (chunk) { + if (this._errored || this._terminated) { + return PromiseCtor.reject(new TypeError("TransformStream is not writable")); + } + + var transformer = this._transformer; + var controller = this._controller; + var result; + + try { + if (typeof transformer.transform === "function") { + result = transformer.transform(chunk, controller); + } else { + controller.enqueue(chunk); + } + } catch (e) { + this._error(e); + return PromiseCtor.reject(e); + } + + var self = this; + return PromiseCtor.resolve(result).then(function () { + return undefined; + }, function (e) { + self._error(e); + throw e; + }); + }; + + PolyfillTransformStream.prototype._flush = function () { + if (this._errored || this._terminated) { + return PromiseCtor.reject(new TypeError("TransformStream is not writable")); + } + + var transformer = this._transformer; + var controller = this._controller; + var result; + + try { + if (typeof transformer.flush === "function") { + result = transformer.flush(controller); + } + } catch (e) { + this._error(e); + return PromiseCtor.reject(e); + } + + var self = this; + return PromiseCtor.resolve(result).then(function () { + self._closeReadable(); + }, function (e) { + self._error(e); + throw e; + }); + }; + + PolyfillTransformStream.prototype._closeReadable = function () { + if (this._readableClosed) { + return; + } + this._readableClosed = true; + + var controller = this._controller._readableController; + if (!controller) { + return; + } + try { + controller.close(); + } catch (e) {} + }; + + PolyfillTransformStream.prototype._error = function (reason) { + if (this._errored) { + return; + } + this._errored = true; + + var controller = this._controller._readableController; + if (controller) { + try { + controller.error(reason); + } catch (e) {} + } else { + this._pendingError = reason; + this._hasPendingError = true; + } + + if (this._writable && typeof this._writable._error === "function") { + this._writable._error(reason); + } + }; + + PolyfillTransformStream.prototype._terminate = function () { + if (this._terminated) { + return; + } + this._terminated = true; + this._closeReadable(); + if (this._writable && typeof this._writable._error === "function") { + this._writable._error(new TypeError("TransformStream terminated")); + } + }; + + global.TransformStream = PolyfillTransformStream; + if (typeof global.WritableStream !== "function") { + global.WritableStream = PolyfillWritableStream; + } + try { + global.__internalUserscriptsTransformStreamPolyfill = true; + } catch (e) {} +})(); diff --git a/browser/internaluserscripts/bundled-scripts/webauthn-microsoft-shim.user.js b/browser/internaluserscripts/bundled-scripts/webauthn-microsoft-shim.user.js new file mode 100644 index 0000000000..1ca9f02ad2 --- /dev/null +++ b/browser/internaluserscripts/bundled-scripts/webauthn-microsoft-shim.user.js @@ -0,0 +1,80 @@ +// 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/. +// ==UserScript== +// @name WebAuthn Shim for Microsoft +// @namespace internal-userscripts +// @description Forces WebAuthn capability checks to report unsupported on Microsoft auth domains. +// @match https://*.microsoft.com/* +// @match https://*.live.com/* +// @match https://*.msauth.net/* +// @match https://*.visualstudio.com/* +// @grant none +// @run-at document-start +// ==/UserScript== +(function () { + "use strict"; + + var global = typeof window !== "undefined" ? window : null; + if (!global || !global.location) { + return; + } + + if (global.location.protocol !== "https:") { + return; + } + + var host = String(global.location.hostname || "").toLowerCase(); + function matchesDomain(domain) { + return host === domain || host.slice(-(domain.length + 1)) === "." + domain; + } + + if ( + !matchesDomain("microsoft.com") && + !matchesDomain("live.com") && + !matchesDomain("msauth.net") && + !matchesDomain("visualstudio.com") + ) { + return; + } + + function installShim() { + if (typeof navigator.credentials === "undefined") { + var fake = { + create: function () { + return Promise.reject( + new DOMException("WebAuthn not supported", "NotSupportedError"), + ); + }, + get: function () { + return Promise.reject( + new DOMException("WebAuthn not supported", "NotSupportedError"), + ); + }, + }; + + try { + navigator.credentials = fake; + } catch (e) { + try { + Object.defineProperty(navigator, "credentials", { + value: fake, + }); + } catch (e2) {} + } + } + + if (typeof global.PublicKeyCredential === "undefined") { + global.PublicKeyCredential = function () {}; + global.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable = + function () { + return Promise.resolve(false); + }; + } + } + + installShim(); + try { + global.__internalUserscriptsWebAuthnMicrosoftShim = true; + } catch (e) {} +})(); diff --git a/browser/internaluserscripts/components/internaluserscripts.js b/browser/internaluserscripts/components/internaluserscripts.js new file mode 100644 index 0000000000..d60ce90ea9 --- /dev/null +++ b/browser/internaluserscripts/components/internaluserscripts.js @@ -0,0 +1,278 @@ +/* This source file is licensed under the MIT License. + * A copy of the MIT License should have been distributed with this + * file. If not, see https://opensource.org/licenses/MIT. + */ + +if (typeof Cc === "undefined") { + var Cc = Components.classes; +} +if (typeof Ci === "undefined") { + var Ci = Components.interfaces; +} +if (typeof Cu === "undefined") { + var Cu = Components.utils; +} + +const { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); +const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}); + +const PREF_ENABLED = "browser.internal-userscripts.enabled"; + +function InternalUserscriptsService() { + this.wrappedJSObject = this; +} + +InternalUserscriptsService.prototype = { + classDescription: "Internal Userscripts Loader", + classID: Components.ID("{d8b4bd27-b458-4417-8dfe-3b80bb6375bc}"), + contractID: "@internaluserscripts.mozdev.org/service;1", + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), + + observe: function (subject, topic, data) { + if (topic === "profile-after-change") { + this._startup(); + } else if (topic === "quit-application") { + this._shutdown(); + } else if (topic === "nsPref:changed" && data === PREF_ENABLED) { + this._updateObserverState(); + } else if (topic === "document-element-inserted") { + let win = subject && subject.defaultView; + if (!win) { + return; + } + let self = this; + Services.tm.mainThread.dispatch(function () { + self._inject(win); + }, Ci.nsIThread.DISPATCH_NORMAL); + } + }, + + _startup: function () { + Services.obs.addObserver(this, "quit-application", false); + Services.prefs.addObserver(PREF_ENABLED, this, false); + this._updateObserverState(); + }, + + _shutdown: function () { + try { + Services.obs.removeObserver(this, "document-element-inserted"); + } catch (e) {} + try { + Services.obs.removeObserver(this, "quit-application"); + } catch (e) {} + try { + Services.prefs.removeObserver(PREF_ENABLED, this); + } catch (e) {} + this._observingDocuments = false; + }, + + _updateObserverState: function () { + let enabled = true; + try { + enabled = Services.prefs.getBoolPref(PREF_ENABLED, true); + } catch (e) {} + + if (enabled && !this._observingDocuments) { + Services.obs.addObserver(this, "document-element-inserted", false); + this._observingDocuments = true; + } else if (!enabled && this._observingDocuments) { + try { + Services.obs.removeObserver(this, "document-element-inserted"); + } catch (e) {} + this._observingDocuments = false; + } + }, + + _inject: function (win) { + if (!Services.prefs.getBoolPref(PREF_ENABLED, true)) { + return; + } + if (!win) { + return; + } + try { + let doc = win.document; + let uri = doc && doc.documentURIObject; + if (uri && (uri.schemeIs("chrome") || uri.schemeIs("resource"))) { + return; + } + } catch (e) {} + let contentWin = win.wrappedJSObject || win; + let logPolyfill = function (name, source) { + try { + if ( + contentWin.console && + typeof contentWin.console.info === "function" + ) { + let suffix = source ? " (" + source + ")" : ""; + contentWin.console.info( + "[internal userscripts] Loaded polyfill script: " + name + suffix, + ); + } + } catch (e) {} + }; + + try { + Services.scriptloader.loadSubScript( + "chrome://internaluserscripts/content/bundled-scripts/intl-displaynames-polyfill.user.js", + contentWin, + ); + if (contentWin.__internalUserscriptsIntlDisplayNamesPolyfill) { + logPolyfill("Intl.DisplayNames", "bundled"); + } + } catch (e) { + // ignore + } + + try { + Services.scriptloader.loadSubScript( + "chrome://internaluserscripts/content/bundled-scripts/intl-listformat-polyfill.user.js", + contentWin, + ); + if (contentWin.__internalUserscriptsIntlListFormatPolyfill) { + logPolyfill("Intl.ListFormat", "bundled"); + } + } catch (e) { + // ignore + } + + try { + Services.scriptloader.loadSubScript( + "chrome://internaluserscripts/content/bundled-scripts/intl-relativetimeformat-formattoparts-polyfill.user.js", + contentWin, + ); + if ( + contentWin.__internalUserscriptsIntlRelativeTimeFormatFormatToPartsPolyfill + ) { + logPolyfill("Intl.RelativeTimeFormat.formatToParts", "bundled"); + } + } catch (e) { + // ignore + } + + try { + Services.scriptloader.loadSubScript( + "chrome://internaluserscripts/content/bundled-scripts/intl-segmenter-polyfill.user.js", + contentWin, + ); + if (contentWin.__internalUserscriptsIntlSegmenterPolyfill) { + logPolyfill("Intl.Segmenter", "bundled"); + } + } catch (e) { + // ignore + } + + try { + Services.scriptloader.loadSubScript( + "chrome://internaluserscripts/content/bundled-scripts/webauthn-microsoft-shim.user.js", + contentWin, + ); + if (contentWin.__internalUserscriptsWebAuthnMicrosoftShim) { + logPolyfill("WebAuthn Microsoft unsupported shim", "bundled"); + } + } catch (e) { + // ignore + } + + try { + Services.scriptloader.loadSubScript( + "chrome://internaluserscripts/content/bundled-scripts/elementfrompoint-finite-polyfill.user.js", + contentWin, + ); + if (contentWin.__internalUserscriptsElementFromPointFinitePolyfill) { + logPolyfill("Document.elementFromPoint finite-args shim", "bundled"); + } + } catch (e) { + // ignore + } + + try { + Services.scriptloader.loadSubScript( + "chrome://internaluserscripts/content/bundled-scripts/getanimations-polyfill.user.js", + contentWin, + ); + if (contentWin.__internalUserscriptsGetAnimationsPolyfill) { + logPolyfill("getAnimations", "bundled"); + } + } catch (e) { + // ignore + } + + try { + Services.scriptloader.loadSubScript( + "chrome://internaluserscripts/content/bundled-scripts/imagedecode-polyfill.user.js", + contentWin, + ); + if (contentWin.__internalUserscriptsImageDecodePolyfill) { + logPolyfill("HTMLImageElement.decode", "bundled"); + } + } catch (e) { + // ignore + } + + try { + Services.scriptloader.loadSubScript( + "chrome://internaluserscripts/content/bundled-scripts/transformstream-polyfill.user.js", + contentWin, + ); + if (contentWin.__internalUserscriptsTransformStreamPolyfill) { + logPolyfill("TransformStream", "bundled"); + } + } catch (e) { + // ignore + } + + try { + Services.scriptloader.loadSubScript( + "chrome://internaluserscripts/content/bundled-scripts/textencoderstream-polyfill.user.js", + contentWin, + ); + if (contentWin.__internalUserscriptsTextEncoderStreamPolyfill) { + logPolyfill("TextEncoderStream", "bundled"); + } + } catch (e) { + // ignore + } + + try { + Services.scriptloader.loadSubScript( + "chrome://internaluserscripts/content/bundled-scripts/textdecoderstream-polyfill.user.js", + contentWin, + ); + if (contentWin.__internalUserscriptsTextDecoderStreamPolyfill) { + logPolyfill("TextDecoderStream", "bundled"); + } + } catch (e) { + // ignore + } + + try { + Services.scriptloader.loadSubScript( + "chrome://internaluserscripts/content/bundled-scripts/readablestream-pipeto-polyfill.user.js", + contentWin, + ); + if (contentWin.__internalUserscriptsReadableStreamPipeToPolyfill) { + logPolyfill("ReadableStream.pipeTo", "bundled"); + } + } catch (e) { + // ignore + } + + try { + Services.scriptloader.loadSubScript( + "chrome://internaluserscripts/content/bundled-scripts/readablestream-pipethrough-polyfill.user.js", + contentWin, + ); + if (contentWin.__internalUserscriptsReadableStreamPipeThroughPolyfill) { + logPolyfill("ReadableStream.pipeThrough", "bundled"); + } + } catch (e) { + // ignore + } + }, +}; + +var NSGetFactory = XPCOMUtils.generateNSGetFactory([ + InternalUserscriptsService, +]); diff --git a/browser/internaluserscripts/components/internaluserscripts.manifest b/browser/internaluserscripts/components/internaluserscripts.manifest new file mode 100644 index 0000000000..9a43b19bae --- /dev/null +++ b/browser/internaluserscripts/components/internaluserscripts.manifest @@ -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/. + +component {d8b4bd27-b458-4417-8dfe-3b80bb6375bc} internaluserscripts.js +contract @internaluserscripts.mozdev.org/service;1 {d8b4bd27-b458-4417-8dfe-3b80bb6375bc} +category profile-after-change internaluserscripts @internaluserscripts.mozdev.org/service;1 diff --git a/browser/internaluserscripts/defaults/preferences/internaluserscripts.js b/browser/internaluserscripts/defaults/preferences/internaluserscripts.js new file mode 100644 index 0000000000..f87d1d0121 --- /dev/null +++ b/browser/internaluserscripts/defaults/preferences/internaluserscripts.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/. + +pref("browser.internal-userscripts.enabled", true); diff --git a/browser/internaluserscripts/internaluserscripts.manifest b/browser/internaluserscripts/internaluserscripts.manifest new file mode 100644 index 0000000000..fea7393c1b --- /dev/null +++ b/browser/internaluserscripts/internaluserscripts.manifest @@ -0,0 +1,6 @@ +# 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/. + +content internaluserscripts internaluserscripts/content/ +resource internaluserscripts internaluserscripts/content/ diff --git a/browser/internaluserscripts/jar.mn b/browser/internaluserscripts/jar.mn new file mode 100644 index 0000000000..dbf882506f --- /dev/null +++ b/browser/internaluserscripts/jar.mn @@ -0,0 +1,9 @@ +# 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/. + +internaluserscripts.jar: +% content internaluserscripts %content/ +% resource internaluserscripts %content/ + + content/bundled-scripts/ (bundled-scripts/*) diff --git a/browser/internaluserscripts/moz.build b/browser/internaluserscripts/moz.build new file mode 100644 index 0000000000..40f2b86f9d --- /dev/null +++ b/browser/internaluserscripts/moz.build @@ -0,0 +1,33 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# 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/. + +JAR_MANIFESTS += ['jar.mn'] + +FINAL_TARGET = 'dist/bin/browser' +FINAL_TARGET_FILES.components += [ + 'components/internaluserscripts.js', + 'components/internaluserscripts.manifest', +] + +FINAL_TARGET_FILES['defaults/preferences'] += [ + 'defaults/preferences/internaluserscripts.js', +] + +# Copy any bundled internal user scripts into the app dir for automatic import. +FINAL_TARGET_FILES['internal-userscripts'] += [ + 'bundled-scripts/elementfrompoint-finite-polyfill.user.js', + 'bundled-scripts/getanimations-polyfill.user.js', + 'bundled-scripts/imagedecode-polyfill.user.js', + 'bundled-scripts/intl-displaynames-polyfill.user.js', + 'bundled-scripts/intl-listformat-polyfill.user.js', + 'bundled-scripts/intl-relativetimeformat-formattoparts-polyfill.user.js', + 'bundled-scripts/intl-segmenter-polyfill.user.js', + 'bundled-scripts/readablestream-pipethrough-polyfill.user.js', + 'bundled-scripts/readablestream-pipeto-polyfill.user.js', + 'bundled-scripts/textdecoderstream-polyfill.user.js', + 'bundled-scripts/textencoderstream-polyfill.user.js', + 'bundled-scripts/transformstream-polyfill.user.js', + 'bundled-scripts/webauthn-microsoft-shim.user.js', +] diff --git a/browser/moz.build b/browser/moz.build index 2270476f9d..6e339e1888 100644 --- a/browser/moz.build +++ b/browser/moz.build @@ -10,6 +10,7 @@ SPHINX_TREES['browser'] = 'docs' DIRS += [ 'base', 'components', + 'internaluserscripts', 'locales', 'modules', 'themes',