1 /* -*- mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
8 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
11 ChromeUtils.defineESModuleGetters(lazy, {
12 ReaderMode: "resource://gre/modules/ReaderMode.sys.mjs",
13 Region: "resource://gre/modules/Region.sys.mjs",
16 XPCOMUtils.defineLazyPreferenceGetter(
18 "INVALID_SHAREABLE_SCHEMES",
19 "services.sync.engine.tabs.filteredSchemes",
23 return new Set(val.split("|"));
26 XPCOMUtils.defineLazyPreferenceGetter(
28 "FXVIEW_SEARCH_ENABLED",
29 "browser.firefox-view.search.enabled"
32 ChromeUtils.defineLazyGetter(lazy, "gLocalization", () => {
33 return new Localization(["toolkit/global/browser-utils.ftl"], true);
36 function stringPrefToSet(prefVal) {
40 .split(/\s*,\s*/g) // split on commas, ignoring whitespace
41 .filter(v => !!v) // discard any falsey values
45 export var BrowserUtils = {
47 * Return or create a principal with the content of one, and the originAttributes
48 * of an existing principal (e.g. on a docshell, where the originAttributes ought
49 * not to change, that is, we should keep the userContextId, privateBrowsingId,
50 * etc. the same when changing the principal).
53 * The principal whose content/null/system-ness we want.
54 * @param existingPrincipal
55 * The principal whose originAttributes we want, usually the current
56 * principal of a docshell.
57 * @return an nsIPrincipal that matches the content/null/system-ness of the first
58 * param, and the originAttributes of the second.
60 principalWithMatchingOA(principal, existingPrincipal) {
61 // Don't care about system principals:
62 if (principal.isSystemPrincipal) {
66 // If the originAttributes already match, just return the principal as-is.
67 if (existingPrincipal.originSuffix == principal.originSuffix) {
71 let secMan = Services.scriptSecurityManager;
72 if (principal.isContentPrincipal) {
73 return secMan.principalWithOA(
75 existingPrincipal.originAttributes
79 if (principal.isNullPrincipal) {
80 return secMan.createNullPrincipal(existingPrincipal.originAttributes);
83 "Can't change the originAttributes of an expanded principal!"
88 * Returns true if |mimeType| is text-based, or false otherwise.
91 * The MIME type to check.
93 mimeTypeIsTextBased(mimeType) {
95 mimeType.startsWith("text/") ||
96 mimeType.endsWith("+xml") ||
97 mimeType.endsWith("+json") ||
98 mimeType == "application/x-javascript" ||
99 mimeType == "application/javascript" ||
100 mimeType == "application/json" ||
101 mimeType == "application/xml"
106 * Returns true if we can show a find bar, including FAYT, for the specified
107 * document location. The location must not be in a blocklist of specific
108 * "about:" pages for which find is disabled.
110 * This can be called from the parent process or from content processes.
112 canFindInPage(location) {
114 !location.startsWith("about:preferences") &&
115 !location.startsWith("about:settings") &&
116 !location.startsWith("about:logins") &&
117 !(location.startsWith("about:firefoxview") && lazy.FXVIEW_SEARCH_ENABLED)
121 isFindbarVisible(docShell) {
122 const FINDER_SYS_MJS = "resource://gre/modules/Finder.sys.mjs";
124 Cu.isESModuleLoaded(FINDER_SYS_MJS) &&
125 ChromeUtils.importESModule(FINDER_SYS_MJS).Finder.isFindbarVisible(
132 * Returns a Promise which resolves when the given observer topic has been
135 * @param {string} topic
136 * The topic to observe.
137 * @param {function(nsISupports, string)} [test]
138 * An optional test function which, when called with the
139 * observer's subject and data, should return true if this is the
140 * expected notification, false otherwise.
141 * @returns {Promise<object>}
143 promiseObserved(topic, test = () => true) {
144 return new Promise(resolve => {
145 let observer = (subject, topic, data) => {
146 if (test(subject, data)) {
147 Services.obs.removeObserver(observer, topic);
148 resolve({ subject, data });
151 Services.obs.addObserver(observer, topic);
155 formatURIStringForDisplay(uriString, options = {}) {
157 return this.formatURIForDisplay(Services.io.newURI(uriString), options);
163 formatURIForDisplay(uri, options = {}) {
164 let { showInsecureHTTP = false } = options;
165 switch (uri.scheme) {
167 let innerURI = uri.spec.substring("view-source:".length);
168 return this.formatURIStringForDisplay(innerURI, options);
172 let host = uri.displayHostPort;
173 if (!showInsecureHTTP && host.startsWith("www.")) {
174 host = Services.eTLD.getSchemelessSite(uri);
176 if (showInsecureHTTP && uri.scheme == "http") {
177 return "http://" + host;
181 return "about:" + uri.filePath;
184 let url = new URL(uri.specIgnoringRef);
185 // _If_ we find a non-null origin, report that.
186 if (url.origin && url.origin != "null") {
187 return this.formatURIStringForDisplay(url.origin, options);
189 // otherwise, fall through...
191 console.error("Invalid blob URI passed to formatURIForDisplay: ", ex);
193 /* For blob URIs without an origin, fall through and use the data URI
194 * logic (shows just "(data)", localized). */
196 return lazy.gLocalization.formatValueSync("browser-utils-url-data");
197 case "moz-extension":
198 let policy = WebExtensionPolicy.getByURI(uri);
199 return lazy.gLocalization.formatValueSync(
200 "browser-utils-url-extension",
201 { extension: policy?.name.trim() || uri.spec }
209 let url = uri.QueryInterface(Ci.nsIURL);
210 // Just the filename if we have one:
214 // We won't get a filename for a path that looks like:
216 // So try the directory name:
218 let parts = url.directory.split("/");
219 // Pop off any empty bits at the end:
221 while (!last && parts.length) {
232 return uri.asciiHost || uri.spec;
235 // Given a URL returns a (possibly transformed) URL suitable for sharing, or null if
236 // no such URL can be obtained.
237 getShareableURL(url) {
242 // Carve out an exception for about:reader.
243 if (url.spec.startsWith("about:reader?")) {
244 url = Services.io.newURI(lazy.ReaderMode.getOriginalUrl(url.spec));
246 // Disallow sharing URLs with more than 65535 characters.
247 if (url.spec.length > 65535) {
250 // Use the same preference as synced tabs to disable what kind
251 // of tabs we can send to another device
252 return lazy.INVALID_SHAREABLE_SCHEMES.has(url.scheme) ? null : url;
256 * Extracts linkNode and href for a click event.
260 * @return [href, linkNode, linkPrincipal].
262 * @note linkNode will be null if the click wasn't on an anchor
263 * element. This includes SVG links, because callers expect |node|
264 * to behave like an <a> element, which SVG links (XLink) don't.
266 hrefAndLinkNodeForClickEvent(event) {
267 // We should get a window off the event, and bail if not:
268 let content = event.view || event.composedTarget?.ownerGlobal;
269 if (!content?.HTMLAnchorElement) {
272 function isHTMLLink(aNode) {
273 // Be consistent with what nsContextMenu.js does.
275 (content.HTMLAnchorElement.isInstance(aNode) && aNode.href) ||
276 (content.HTMLAreaElement.isInstance(aNode) && aNode.href) ||
277 content.HTMLLinkElement.isInstance(aNode)
281 let node = event.composedTarget;
282 while (node && !isHTMLLink(node)) {
283 node = node.flattenedTreeParentNode;
287 return [node.href, node, node.ownerDocument.nodePrincipal];
290 // If there is no linkNode, try simple XLink.
292 node = event.composedTarget;
293 while (node && !href) {
295 node.nodeType == content.Node.ELEMENT_NODE &&
296 (node.localName == "a" ||
297 node.namespaceURI == "http://www.w3.org/1998/Math/MathML")
300 node.getAttribute("href") ||
301 node.getAttributeNS("http://www.w3.org/1999/xlink", "href");
303 baseURI = node.ownerDocument.baseURIObject;
307 node = node.flattenedTreeParentNode;
310 // In case of XLink, we don't return the node we got href from since
311 // callers expect <a>-like elements.
312 // Note: makeURI() will throw if aUri is not a valid URI.
314 href ? Services.io.newURI(href, null, baseURI).spec : null,
316 node && node.ownerDocument.nodePrincipal,
321 * whereToOpenLink() looks at an event to decide where to open a link.
323 * The event may be a mouse event (click, double-click, middle-click) or keypress event (enter).
325 * On Windows, the modifiers are:
326 * Ctrl new tab, selected
328 * Ctrl+Shift new tab, in background
331 * Middle-clicking is the same as Ctrl+clicking (it opens a new tab).
334 * - Alt is ignored for menu items selected using the keyboard so you don't accidentally save stuff.
335 * (Currently, the Alt isn't sent here at all for menu items, but that will change in bug 126189.)
336 * - Alt is hard to use in context menus, because pressing Alt closes the menu.
337 * - Alt can't be used on the bookmarks toolbar because Alt is used for "treat this as something draggable".
338 * - The button is ignored for the middle-click-paste-URL feature, since it's always a middle-click.
340 * @param e {Event|Object} Event or JSON Object
341 * @param ignoreButton {Boolean}
342 * @param ignoreAlt {Boolean}
343 * @returns {"current" | "tabshifted" | "tab" | "save" | "window"}
345 whereToOpenLink(e, ignoreButton, ignoreAlt) {
346 // This method must treat a null event like a left click without modifier keys (i.e.
347 // e = { shiftKey:false, ctrlKey:false, metaKey:false, altKey:false, button:0 })
348 // for compatibility purposes.
353 e = this.getRootEvent(e);
355 var shift = e.shiftKey;
356 var ctrl = e.ctrlKey;
357 var meta = e.metaKey;
358 var alt = e.altKey && !ignoreAlt;
360 // ignoreButton allows "middle-click paste" to use function without always opening in a new window.
361 let middle = !ignoreButton && e.button == 1;
362 let middleUsesTabs = Services.prefs.getBoolPref(
363 "browser.tabs.opentabfor.middleclick",
366 let middleUsesNewWindow = Services.prefs.getBoolPref(
367 "middlemouse.openNewWindow",
371 // Don't do anything special with right-mouse clicks. They're probably clicks on context menu items.
373 // See also nsWindowWatcher::GetWindowOpenLocation in
374 // toolkit/components/windowwatcher/nsWindowWatcher.cpp
376 var metaKey = AppConstants.platform == "macosx" ? meta : ctrl;
377 if (metaKey || (middle && middleUsesTabs)) {
378 return shift ? "tabshifted" : "tab";
381 if (alt && Services.prefs.getBoolPref("browser.altClickSave", false)) {
385 if (shift || (middle && !middleUsesTabs && middleUsesNewWindow)) {
392 // Utility function to check command events for potential middle-click events
393 // from checkForMiddleClick and unwrap them.
394 getRootEvent(aEvent) {
395 // Part of the fix for Bug 1523813.
396 // Middle-click events arrive here wrapped in different numbers (1-2) of
397 // command events, depending on the button originally clicked.
401 let tempEvent = aEvent;
402 while (tempEvent.sourceEvent) {
403 if (tempEvent.sourceEvent.button == 1) {
404 aEvent = tempEvent.sourceEvent;
407 tempEvent = tempEvent.sourceEvent;
413 * An enumeration of the promotion types that can be passed to shouldShowPromo
416 DEFAULT: 0, // invalid
425 * Should a given promo be shown to the user now, based on things including:
429 * where ads for a particular thing are allowed
430 * where they are illegal
431 * in what regions is the thing being promoted supported?
432 * whether there is an active enterprise policy
433 * settings of specific preferences related to this promo
435 * @param {BrowserUtils.PromoType} promoType - What promo are we checking on?
437 * @return {boolean} - should we display this promo now or not?
439 shouldShowPromo(promoType) {
441 case this.PromoType.VPN:
442 case this.PromoType.FOCUS:
443 case this.PromoType.PIN:
444 case this.PromoType.RELAY:
445 case this.PromoType.COOKIE_BANNERS:
448 throw new Error("Unknown promo type: ", promoType);
451 const info = PromoInfo[promoType];
453 !info.enabledPref || Services.prefs.getBoolPref(info.enabledPref, true);
455 const homeRegion = lazy.Region.home || "";
456 const currentRegion = lazy.Region.current || "";
458 let inSupportedRegion = true;
459 if ("supportedRegions" in info.lazyStringSetPrefs) {
460 const supportedRegions =
461 info.lazyStringSetPrefs.supportedRegions.lazyValue;
463 supportedRegions.has(currentRegion.toLowerCase()) ||
464 supportedRegions.has(homeRegion.toLowerCase());
467 const avoidAdsRegions =
468 info.lazyStringSetPrefs.disallowedRegions?.lazyValue;
470 // Don't show promo if there's an active enterprise policy
471 const noActivePolicy =
472 info.showForEnterprise ||
473 !Services.policies ||
474 Services.policies.status !== Services.policies.ACTIVE;
476 // Promos may add custom checks that must pass.
477 const passedExtraCheck = !info.extraCheck || info.extraCheck();
481 !avoidAdsRegions?.has(homeRegion.toLowerCase()) &&
482 !avoidAdsRegions?.has(currentRegion.toLowerCase()) &&
483 !info.illegalRegions.includes(homeRegion.toLowerCase()) &&
484 !info.illegalRegions.includes(currentRegion.toLowerCase()) &&
492 * @deprecated in favor of shouldShowPromo
494 shouldShowVPNPromo() {
495 return this.shouldShowPromo(this.PromoType.VPN);
498 // Return true if Send to Device emails are supported for user's locale
499 sendToDeviceEmailsSupported() {
500 const userLocale = Services.locale.appLocaleAsBCP47.toLowerCase();
501 return this.emailSupportedLocales.has(userLocale);
506 * A table of promos used by shouldShowPromo to decide whether or not to show.
507 * Each entry defines the criteria for a given promo, and also houses lazy
508 * getters for specified string set preferences.
511 [BrowserUtils.PromoType.VPN]: {
512 enabledPref: "browser.vpn_promo.enabled",
513 lazyStringSetPrefs: {
515 name: "browser.contentblocking.report.vpn_regions",
517 "ca,my,nz,sg,gb,gg,im,io,je,uk,vg,as,mp,pr,um,us,vi,de,fr,at,be,ch,es,it,ie,nl,se,fi,bg,cy,cz,dk,ee,hr,hu,lt,lu,lv,mt,pl,pt,ro,si,sk",
520 name: "browser.vpn_promo.disallowed_regions",
521 default: "ae,by,cn,cu,iq,ir,kp,om,ru,sd,sy,tm,tr",
524 //See https://github.com/search?q=repo%3Amozilla%2Fbedrock+VPN_EXCLUDED_COUNTRY_CODES&type=code
541 [BrowserUtils.PromoType.FOCUS]: {
542 enabledPref: "browser.promo.focus.enabled",
543 lazyStringSetPrefs: {
544 // there are no particular limitions to where it is "supported",
545 // so we leave out the supported pref
547 name: "browser.promo.focus.disallowed_regions",
551 illegalRegions: ["cn"],
553 [BrowserUtils.PromoType.PIN]: {
554 enabledPref: "browser.promo.pin.enabled",
555 lazyStringSetPrefs: {},
558 [BrowserUtils.PromoType.RELAY]: {
559 lazyStringSetPrefs: {},
561 // Returns true if user is using the FxA "production" instance, or returns
562 // false for custom FxA instance (such as accounts.firefox.com.cn for the
563 // China repack) which doesn't support authentication for addons like Relay.
565 !Services.prefs.getCharPref("identity.fxaccounts.autoconfig.uri", "") &&
567 "identity.fxaccounts.remote.root",
568 "identity.fxaccounts.auth.uri",
569 "identity.fxaccounts.remote.oauth.uri",
570 "identity.fxaccounts.remote.profile.uri",
571 "identity.fxaccounts.remote.pairing.uri",
572 "identity.sync.tokenserver.uri",
573 ].every(pref => !Services.prefs.prefHasUserValue(pref)),
575 [BrowserUtils.PromoType.COOKIE_BANNERS]: {
576 enabledPref: "browser.promo.cookiebanners.enabled",
577 lazyStringSetPrefs: {},
579 showForEnterprise: true,
584 * Finish setting up the PromoInfo data structure by attaching lazy prefs getters
585 * as specified in the structure. (the object for each pref in the lazyStringSetPrefs
586 * gets a `lazyValue` property attached to it).
588 for (let promo of Object.values(PromoInfo)) {
589 for (let prefObj of Object.values(promo.lazyStringSetPrefs)) {
590 XPCOMUtils.defineLazyPreferenceGetter(
601 XPCOMUtils.defineLazyPreferenceGetter(
603 "navigationRequireUserInteraction",
604 "browser.navigation.requireUserInteraction",
608 XPCOMUtils.defineLazyPreferenceGetter(
610 "emailSupportedLocales",
611 "browser.send_to_device_locales",
612 "de,en-GB,en-US,es-AR,es-CL,es-ES,es-MX,fr,id,pl,pt-BR,ru,zh-TW",