/* -*- mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* 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/. */
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ReaderMode: "moz-src:///toolkit/components/reader/ReaderMode.sys.mjs",
Region: "resource://gre/modules/Region.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "CatManListenerManager", () => {
const CatManListenerManager = {
cachedModules: {},
cachedListeners: {},
// All 3 category manager notifications will have the category name
// as the `data` part of the observer notification.
observe(_subject, _topic, categoryName) {
delete this.cachedListeners[categoryName];
},
/**
* Fetch and parse category manager consumers for a given category name.
* Will use cachedListeners for the given category name if they exist.
*/
getListeners(categoryName) {
if (Object.hasOwn(this.cachedListeners, categoryName)) {
return this.cachedListeners[categoryName];
}
let rv = Array.from(
Services.catMan.enumerateCategory(categoryName),
({ data: module, value }) => {
try {
let [objName, method] = value.split(".");
let fn = (...args) => {
if (!Object.hasOwn(this.cachedModules, module)) {
this.cachedModules[module] = ChromeUtils.importESModule(module);
}
let obj = this.cachedModules[module][objName];
if (!obj) {
throw new Error(
`Could not access ${objName} in ${module}. Is it exported?`
);
}
if (typeof obj[method] != "function") {
throw new Error(
`${objName}.${method} in ${module} is not a function.`
);
}
return this.cachedModules[module][objName][method](...args);
};
fn._descriptiveName = value;
return fn;
} catch (ex) {
console.error(
`Error processing category manifest for ${module}: ${value}`,
ex
);
return null;
}
}
);
// Remove any null entries.
rv = rv.filter(l => !!l);
this.cachedListeners[categoryName] = rv;
return rv;
},
};
Services.obs.addObserver(
CatManListenerManager,
"xpcom-category-entry-removed"
);
Services.obs.addObserver(CatManListenerManager, "xpcom-category-entry-added");
Services.obs.addObserver(CatManListenerManager, "xpcom-category-cleared");
return CatManListenerManager;
});
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"INVALID_SHAREABLE_SCHEMES",
"services.sync.engine.tabs.filteredSchemes",
"",
null,
val => {
return new Set(val.split("|"));
}
);
ChromeUtils.defineLazyGetter(lazy, "gLocalization", () => {
return new Localization(["toolkit/global/browser-utils.ftl"], true);
});
function stringPrefToSet(prefVal) {
return new Set(
prefVal
.toLowerCase()
.split(/\s*,\s*/g) // split on commas, ignoring whitespace
.filter(v => !!v) // discard any falsey values
);
}
/**
* @class BrowserUtils
*
* A motley collection of utilities (also known as a dumping ground).
*
* Please avoid expanding this if possible.
*/
export var BrowserUtils = {
/**
* Return or create a principal with the content of one, and the originAttributes
* of an existing principal (e.g. on a docshell, where the originAttributes ought
* not to change, that is, we should keep the userContextId, privateBrowsingId,
* etc. the same when changing the principal).
*
* @param {nsIPrincipal} principal
* The principal whose content/null/system-ness we want.
* @param {nsIPrincipal} existingPrincipal
* The principal whose originAttributes we want, usually the current
* principal of a docshell.
* @returns {nsIPrincipal} an nsIPrincipal that matches the content/null/system-ness of the first
* param, and the originAttributes of the second.
*/
principalWithMatchingOA(principal, existingPrincipal) {
// Don't care about system principals:
if (principal.isSystemPrincipal) {
return principal;
}
// If the originAttributes already match, just return the principal as-is.
if (existingPrincipal.originSuffix == principal.originSuffix) {
return principal;
}
let secMan = Services.scriptSecurityManager;
if (principal.isContentPrincipal) {
return secMan.principalWithOA(
principal,
existingPrincipal.originAttributes
);
}
if (principal.isNullPrincipal) {
return secMan.createNullPrincipal(existingPrincipal.originAttributes);
}
throw new Error(
"Can't change the originAttributes of an expanded principal!"
);
},
/**
* Copy a link with a text label to the clipboard.
*
* @param {string} url
* The URL we're wanting to copy to the clipboard.
* @param {string} title
* The label/title of the URL
*/
copyLink(url, title) {
this.copyLinks([{ url, title }]);
},
/**
* Copy multiple links to the clipboard. Writes three clipboard flavors:
* text/x-moz-url — "url\ntitle" pairs separated by newlines
* text/html — "title" anchors separated by
* text/plain — plain URLs separated by newlines
*
* @param {Array<{url: string, title: string}>} links
*/
copyLinks(links) {
let htmlEscape = s =>
s
.replace(/&/g, "&")
.replace(/>/g, ">")
.replace(/ `${url}\n${title}`)
.join("\n");
let htmlData = links
.map(({ url, title }) => `${htmlEscape(title)}`)
.join(" \n");
let textData = links.map(({ url }) => url).join("\n");
let xferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(
Ci.nsITransferable
);
xferable.init(null);
for (let [type, data] of [
// This order is _important_! It controls how this and other applications
// select data to be inserted based on type.
["text/x-moz-url", mozURLData],
["text/html", htmlData],
["text/plain", textData],
]) {
let str = Cc["@mozilla.org/supports-string;1"].createInstance(
Ci.nsISupportsString
);
str.data = data;
xferable.addDataFlavor(type);
xferable.setTransferData(type, str);
}
Services.clipboard.setData(
xferable,
null,
Ci.nsIClipboard.kGlobalClipboard
);
},
/**
* Returns true if |mimeType| is text-based, or false otherwise.
*
* @param {string} mimeType
* The MIME type to check.
*/
mimeTypeIsTextBased(mimeType) {
return (
mimeType.startsWith("text/") ||
mimeType.endsWith("+xml") ||
mimeType.endsWith("+json") ||
mimeType == "application/x-javascript" ||
mimeType == "application/javascript" ||
mimeType == "application/json" ||
mimeType == "application/xml"
);
},
/**
* Returns true if we can show a find bar, including FAYT, for the specified
* document location. The location must not be in a blocklist of specific
* "about:" pages for which find is disabled.
*
* This can be called from the parent process or from content processes.
*/
canFindInPage(location) {
return (
!location.startsWith("about:preferences") &&
!location.startsWith("about:settings") &&
!location.startsWith("about:logins") &&
!location.startsWith("about:firefoxview")
);
},
isFindbarVisible(docShell) {
const FINDER_SYS_MJS = "resource://gre/modules/Finder.sys.mjs";
return (
Cu.isESModuleLoaded(FINDER_SYS_MJS) &&
ChromeUtils.importESModule(FINDER_SYS_MJS).Finder.isFindbarVisible(
docShell
)
);
},
/**
* Returns a Promise which resolves when the given observer topic has been
* observed.
*
* @param {string} topic
* The topic to observe.
* @param {(subject: nsISupports, data: string) => boolean} [test]
* An optional test function which, when called with the
* observer's subject and data, should return true if this is the
* expected notification, false otherwise.
* @returns {Promise