Backed out 2 changesets (bug 1908320) for causing wr failures on align-items-baseline...
[gecko.git] / toolkit / modules / BrowserUtils.sys.mjs
blob96009d2a88d414563ef526f3d2f098888a79a8c4
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";
10 const lazy = {};
11 ChromeUtils.defineESModuleGetters(lazy, {
12   ReaderMode: "resource://gre/modules/ReaderMode.sys.mjs",
13   Region: "resource://gre/modules/Region.sys.mjs",
14 });
16 XPCOMUtils.defineLazyPreferenceGetter(
17   lazy,
18   "INVALID_SHAREABLE_SCHEMES",
19   "services.sync.engine.tabs.filteredSchemes",
20   "",
21   null,
22   val => {
23     return new Set(val.split("|"));
24   }
27 ChromeUtils.defineLazyGetter(lazy, "gLocalization", () => {
28   return new Localization(["toolkit/global/browser-utils.ftl"], true);
29 });
31 function stringPrefToSet(prefVal) {
32   return new Set(
33     prefVal
34       .toLowerCase()
35       .split(/\s*,\s*/g) // split on commas, ignoring whitespace
36       .filter(v => !!v) // discard any falsey values
37   );
40 export var BrowserUtils = {
41   /**
42    * Return or create a principal with the content of one, and the originAttributes
43    * of an existing principal (e.g. on a docshell, where the originAttributes ought
44    * not to change, that is, we should keep the userContextId, privateBrowsingId,
45    * etc. the same when changing the principal).
46    *
47    * @param principal
48    *        The principal whose content/null/system-ness we want.
49    * @param existingPrincipal
50    *        The principal whose originAttributes we want, usually the current
51    *        principal of a docshell.
52    * @return an nsIPrincipal that matches the content/null/system-ness of the first
53    *         param, and the originAttributes of the second.
54    */
55   principalWithMatchingOA(principal, existingPrincipal) {
56     // Don't care about system principals:
57     if (principal.isSystemPrincipal) {
58       return principal;
59     }
61     // If the originAttributes already match, just return the principal as-is.
62     if (existingPrincipal.originSuffix == principal.originSuffix) {
63       return principal;
64     }
66     let secMan = Services.scriptSecurityManager;
67     if (principal.isContentPrincipal) {
68       return secMan.principalWithOA(
69         principal,
70         existingPrincipal.originAttributes
71       );
72     }
74     if (principal.isNullPrincipal) {
75       return secMan.createNullPrincipal(existingPrincipal.originAttributes);
76     }
77     throw new Error(
78       "Can't change the originAttributes of an expanded principal!"
79     );
80   },
82   /**
83    * Returns true if |mimeType| is text-based, or false otherwise.
84    *
85    * @param mimeType
86    *        The MIME type to check.
87    */
88   mimeTypeIsTextBased(mimeType) {
89     return (
90       mimeType.startsWith("text/") ||
91       mimeType.endsWith("+xml") ||
92       mimeType.endsWith("+json") ||
93       mimeType == "application/x-javascript" ||
94       mimeType == "application/javascript" ||
95       mimeType == "application/json" ||
96       mimeType == "application/xml"
97     );
98   },
100   /**
101    * Returns true if we can show a find bar, including FAYT, for the specified
102    * document location. The location must not be in a blocklist of specific
103    * "about:" pages for which find is disabled.
104    *
105    * This can be called from the parent process or from content processes.
106    */
107   canFindInPage(location) {
108     return (
109       !location.startsWith("about:preferences") &&
110       !location.startsWith("about:settings") &&
111       !location.startsWith("about:logins") &&
112       !location.startsWith("about:firefoxview")
113     );
114   },
116   isFindbarVisible(docShell) {
117     const FINDER_SYS_MJS = "resource://gre/modules/Finder.sys.mjs";
118     return (
119       Cu.isESModuleLoaded(FINDER_SYS_MJS) &&
120       ChromeUtils.importESModule(FINDER_SYS_MJS).Finder.isFindbarVisible(
121         docShell
122       )
123     );
124   },
126   /**
127    * Returns a Promise which resolves when the given observer topic has been
128    * observed.
129    *
130    * @param {string} topic
131    *        The topic to observe.
132    * @param {function(nsISupports, string)} [test]
133    *        An optional test function which, when called with the
134    *        observer's subject and data, should return true if this is the
135    *        expected notification, false otherwise.
136    * @returns {Promise<object>}
137    */
138   promiseObserved(topic, test = () => true) {
139     return new Promise(resolve => {
140       let observer = (subject, topic, data) => {
141         if (test(subject, data)) {
142           Services.obs.removeObserver(observer, topic);
143           resolve({ subject, data });
144         }
145       };
146       Services.obs.addObserver(observer, topic);
147     });
148   },
150   formatURIStringForDisplay(uriString, options = {}) {
151     try {
152       return this.formatURIForDisplay(Services.io.newURI(uriString), options);
153     } catch (ex) {
154       return uriString;
155     }
156   },
158   formatURIForDisplay(uri, options = {}) {
159     let { showInsecureHTTP = false } = options;
160     switch (uri.scheme) {
161       case "view-source":
162         let innerURI = uri.spec.substring("view-source:".length);
163         return this.formatURIStringForDisplay(innerURI, options);
164       case "http":
165       // Fall through.
166       case "https":
167         let host = uri.displayHostPort;
168         if (!showInsecureHTTP && host.startsWith("www.")) {
169           host = Services.eTLD.getSchemelessSite(uri);
170         }
171         if (showInsecureHTTP && uri.scheme == "http") {
172           return "http://" + host;
173         }
174         return host;
175       case "about":
176         return "about:" + uri.filePath;
177       case "blob":
178         try {
179           let url = new URL(uri.specIgnoringRef);
180           // _If_ we find a non-null origin, report that.
181           if (url.origin && url.origin != "null") {
182             return this.formatURIStringForDisplay(url.origin, options);
183           }
184           // otherwise, fall through...
185         } catch (ex) {
186           console.error("Invalid blob URI passed to formatURIForDisplay: ", ex);
187         }
188       /* For blob URIs without an origin, fall through and use the data URI
189        * logic (shows just "(data)", localized). */
190       case "data":
191         return lazy.gLocalization.formatValueSync("browser-utils-url-data");
192       case "moz-extension":
193         let policy = WebExtensionPolicy.getByURI(uri);
194         return lazy.gLocalization.formatValueSync(
195           "browser-utils-url-extension",
196           { extension: policy?.name.trim() || uri.spec }
197         );
198       case "chrome":
199       case "resource":
200       case "jar":
201       case "file":
202       default:
203         try {
204           let url = uri.QueryInterface(Ci.nsIURL);
205           // Just the filename if we have one:
206           if (url.fileName) {
207             return url.fileName;
208           }
209           // We won't get a filename for a path that looks like:
210           // /foo/bar/baz/
211           // So try the directory name:
212           if (url.directory) {
213             let parts = url.directory.split("/");
214             // Pop off any empty bits at the end:
215             let last;
216             while (!last && parts.length) {
217               last = parts.pop();
218             }
219             if (last) {
220               return last;
221             }
222           }
223         } catch (ex) {
224           console.error(ex);
225         }
226     }
227     return uri.asciiHost || uri.spec;
228   },
230   // Given a URL returns a (possibly transformed) URL suitable for sharing, or null if
231   // no such URL can be obtained.
232   getShareableURL(url) {
233     if (!url) {
234       return null;
235     }
237     // Carve out an exception for about:reader.
238     if (url.spec.startsWith("about:reader?")) {
239       url = Services.io.newURI(lazy.ReaderMode.getOriginalUrl(url.spec));
240     }
241     // Disallow sharing URLs with more than 65535 characters.
242     if (url.spec.length > 65535) {
243       return null;
244     }
245     // Use the same preference as synced tabs to disable what kind
246     // of tabs we can send to another device
247     return lazy.INVALID_SHAREABLE_SCHEMES.has(url.scheme) ? null : url;
248   },
250   /**
251    * Extracts linkNode and href for a click event.
252    *
253    * @param event
254    *        The click event.
255    * @return [href, linkNode, linkPrincipal].
256    *
257    * @note linkNode will be null if the click wasn't on an anchor
258    *       element. This includes SVG links, because callers expect |node|
259    *       to behave like an <a> element, which SVG links (XLink) don't.
260    */
261   hrefAndLinkNodeForClickEvent(event) {
262     // We should get a window off the event, and bail if not:
263     let content = event.view || event.composedTarget?.ownerGlobal;
264     if (!content?.HTMLAnchorElement) {
265       return null;
266     }
267     function isHTMLLink(aNode) {
268       // Be consistent with what nsContextMenu.js does.
269       return (
270         (content.HTMLAnchorElement.isInstance(aNode) && aNode.href) ||
271         (content.HTMLAreaElement.isInstance(aNode) && aNode.href) ||
272         content.HTMLLinkElement.isInstance(aNode)
273       );
274     }
276     let node = event.composedTarget;
277     while (node && !isHTMLLink(node)) {
278       node = node.flattenedTreeParentNode;
279     }
281     if (node) {
282       return [node.href, node, node.ownerDocument.nodePrincipal];
283     }
285     // If there is no linkNode, try simple XLink.
286     let href, baseURI;
287     node = event.composedTarget;
288     while (node && !href) {
289       if (
290         node.nodeType == content.Node.ELEMENT_NODE &&
291         (node.localName == "a" ||
292           node.namespaceURI == "http://www.w3.org/1998/Math/MathML")
293       ) {
294         href =
295           node.getAttribute("href") ||
296           node.getAttributeNS("http://www.w3.org/1999/xlink", "href");
297         if (href) {
298           baseURI = node.ownerDocument.baseURIObject;
299           break;
300         }
301       }
302       node = node.flattenedTreeParentNode;
303     }
305     // In case of XLink, we don't return the node we got href from since
306     // callers expect <a>-like elements.
307     // Note: makeURI() will throw if aUri is not a valid URI.
308     return [
309       href ? Services.io.newURI(href, null, baseURI).spec : null,
310       null,
311       node && node.ownerDocument.nodePrincipal,
312     ];
313   },
315   /**
316    * whereToOpenLink() looks at an event to decide where to open a link.
317    *
318    * The event may be a mouse event (click, double-click, middle-click) or keypress event (enter).
319    *
320    * On Windows, the modifiers are:
321    * Ctrl        new tab, selected
322    * Shift       new window
323    * Ctrl+Shift  new tab, in background
324    * Alt         save
325    *
326    * Middle-clicking is the same as Ctrl+clicking (it opens a new tab).
327    *
328    * Exceptions:
329    * - Alt is ignored for menu items selected using the keyboard so you don't accidentally save stuff.
330    *    (Currently, the Alt isn't sent here at all for menu items, but that will change in bug 126189.)
331    * - Alt is hard to use in context menus, because pressing Alt closes the menu.
332    * - Alt can't be used on the bookmarks toolbar because Alt is used for "treat this as something draggable".
333    * - The button is ignored for the middle-click-paste-URL feature, since it's always a middle-click.
334    *
335    * @param e {Event|Object} Event or JSON Object
336    * @param ignoreButton {Boolean}
337    * @param ignoreAlt {Boolean}
338    * @returns {"current" | "tabshifted" | "tab" | "save" | "window"}
339    */
340   whereToOpenLink(e, ignoreButton, ignoreAlt) {
341     // This method must treat a null event like a left click without modifier keys (i.e.
342     // e = { shiftKey:false, ctrlKey:false, metaKey:false, altKey:false, button:0 })
343     // for compatibility purposes.
344     if (!e) {
345       return "current";
346     }
348     e = this.getRootEvent(e);
350     var shift = e.shiftKey;
351     var ctrl = e.ctrlKey;
352     var meta = e.metaKey;
353     var alt = e.altKey && !ignoreAlt;
355     // ignoreButton allows "middle-click paste" to use function without always opening in a new window.
356     let middle = !ignoreButton && e.button == 1;
357     let middleUsesTabs = Services.prefs.getBoolPref(
358       "browser.tabs.opentabfor.middleclick",
359       true
360     );
361     let middleUsesNewWindow = Services.prefs.getBoolPref(
362       "middlemouse.openNewWindow",
363       false
364     );
366     // Don't do anything special with right-mouse clicks.  They're probably clicks on context menu items.
368     // See also nsWindowWatcher::GetWindowOpenLocation in
369     // toolkit/components/windowwatcher/nsWindowWatcher.cpp
371     var metaKey = AppConstants.platform == "macosx" ? meta : ctrl;
372     if (metaKey || (middle && middleUsesTabs)) {
373       return shift ? "tabshifted" : "tab";
374     }
376     if (alt && Services.prefs.getBoolPref("browser.altClickSave", false)) {
377       return "save";
378     }
380     if (shift || (middle && !middleUsesTabs && middleUsesNewWindow)) {
381       return "window";
382     }
384     return "current";
385   },
387   // Utility function to check command events for potential middle-click events
388   // from checkForMiddleClick and unwrap them.
389   getRootEvent(aEvent) {
390     // Part of the fix for Bug 1523813.
391     // Middle-click events arrive here wrapped in different numbers (1-2) of
392     // command events, depending on the button originally clicked.
393     if (!aEvent) {
394       return aEvent;
395     }
396     let tempEvent = aEvent;
397     while (tempEvent.sourceEvent) {
398       if (tempEvent.sourceEvent.button == 1) {
399         aEvent = tempEvent.sourceEvent;
400         break;
401       }
402       tempEvent = tempEvent.sourceEvent;
403     }
404     return aEvent;
405   },
407   /**
408    * An enumeration of the promotion types that can be passed to shouldShowPromo
409    */
410   PromoType: {
411     DEFAULT: 0, // invalid
412     VPN: 1,
413     RELAY: 2,
414     FOCUS: 3,
415     PIN: 4,
416     COOKIE_BANNERS: 5,
417   },
419   /**
420    * Should a given promo be shown to the user now, based on things including:
421    *
422    *  current region
423    *  home region
424    *  where ads for a particular thing are allowed
425    *  where they are illegal
426    *  in what regions is the thing being promoted supported?
427    *  whether there is an active enterprise policy
428    *  settings of specific preferences related to this promo
429    *
430    * @param {BrowserUtils.PromoType} promoType - What promo are we checking on?
431    *
432    * @return {boolean} - should we display this promo now or not?
433    */
434   shouldShowPromo(promoType) {
435     switch (promoType) {
436       case this.PromoType.VPN:
437       case this.PromoType.FOCUS:
438       case this.PromoType.PIN:
439       case this.PromoType.RELAY:
440       case this.PromoType.COOKIE_BANNERS:
441         break;
442       default:
443         throw new Error("Unknown promo type: ", promoType);
444     }
446     const info = PromoInfo[promoType];
447     const promoEnabled =
448       !info.enabledPref || Services.prefs.getBoolPref(info.enabledPref, true);
450     const homeRegion = lazy.Region.home || "";
451     const currentRegion = lazy.Region.current || "";
453     let inSupportedRegion = true;
454     if ("supportedRegions" in info.lazyStringSetPrefs) {
455       const supportedRegions =
456         info.lazyStringSetPrefs.supportedRegions.lazyValue;
457       inSupportedRegion =
458         supportedRegions.has(currentRegion.toLowerCase()) ||
459         supportedRegions.has(homeRegion.toLowerCase());
460     }
462     const avoidAdsRegions =
463       info.lazyStringSetPrefs.disallowedRegions?.lazyValue;
465     // Don't show promo if there's an active enterprise policy
466     const noActivePolicy =
467       info.showForEnterprise ||
468       !Services.policies ||
469       Services.policies.status !== Services.policies.ACTIVE;
471     // Promos may add custom checks that must pass.
472     const passedExtraCheck = !info.extraCheck || info.extraCheck();
474     return (
475       promoEnabled &&
476       !avoidAdsRegions?.has(homeRegion.toLowerCase()) &&
477       !avoidAdsRegions?.has(currentRegion.toLowerCase()) &&
478       !info.illegalRegions.includes(homeRegion.toLowerCase()) &&
479       !info.illegalRegions.includes(currentRegion.toLowerCase()) &&
480       inSupportedRegion &&
481       noActivePolicy &&
482       passedExtraCheck
483     );
484   },
486   /**
487    * @deprecated in favor of shouldShowPromo
488    */
489   shouldShowVPNPromo() {
490     return this.shouldShowPromo(this.PromoType.VPN);
491   },
493   // Return true if Send to Device emails are supported for user's locale
494   sendToDeviceEmailsSupported() {
495     const userLocale = Services.locale.appLocaleAsBCP47.toLowerCase();
496     return this.emailSupportedLocales.has(userLocale);
497   },
501  * A table of promos used by shouldShowPromo to decide whether or not to show.
502  * Each entry defines the criteria for a given promo, and also houses lazy
503  * getters for specified string set preferences.
504  */
505 let PromoInfo = {
506   [BrowserUtils.PromoType.VPN]: {
507     enabledPref: "browser.vpn_promo.enabled",
508     lazyStringSetPrefs: {
509       supportedRegions: {
510         name: "browser.contentblocking.report.vpn_regions",
511         default:
512           "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",
513       },
514       disallowedRegions: {
515         name: "browser.vpn_promo.disallowed_regions",
516         default: "ae,by,cn,cu,iq,ir,kp,om,ru,sd,sy,tm,tr",
517       },
518     },
519     //See https://github.com/search?q=repo%3Amozilla%2Fbedrock+VPN_EXCLUDED_COUNTRY_CODES&type=code
520     illegalRegions: [
521       "ae",
522       "by",
523       "cn",
524       "cu",
525       "iq",
526       "ir",
527       "kp",
528       "om",
529       "ru",
530       "sd",
531       "sy",
532       "tm",
533       "tr",
534     ],
535   },
536   [BrowserUtils.PromoType.FOCUS]: {
537     enabledPref: "browser.promo.focus.enabled",
538     lazyStringSetPrefs: {
539       // there are no particular limitions to where it is "supported",
540       // so we leave out the supported pref
541       disallowedRegions: {
542         name: "browser.promo.focus.disallowed_regions",
543         default: "cn",
544       },
545     },
546     illegalRegions: ["cn"],
547   },
548   [BrowserUtils.PromoType.PIN]: {
549     enabledPref: "browser.promo.pin.enabled",
550     lazyStringSetPrefs: {},
551     illegalRegions: [],
552   },
553   [BrowserUtils.PromoType.RELAY]: {
554     lazyStringSetPrefs: {},
555     illegalRegions: [],
556     // Returns true if user is using the FxA "production" instance, or returns
557     // false for custom FxA instance (such as accounts.firefox.com.cn for the
558     // China repack) which doesn't support authentication for addons like Relay.
559     extraCheck: () =>
560       !Services.prefs.getCharPref("identity.fxaccounts.autoconfig.uri", "") &&
561       [
562         "identity.fxaccounts.remote.root",
563         "identity.fxaccounts.auth.uri",
564         "identity.fxaccounts.remote.oauth.uri",
565         "identity.fxaccounts.remote.profile.uri",
566         "identity.fxaccounts.remote.pairing.uri",
567         "identity.sync.tokenserver.uri",
568       ].every(pref => !Services.prefs.prefHasUserValue(pref)),
569   },
570   [BrowserUtils.PromoType.COOKIE_BANNERS]: {
571     enabledPref: "browser.promo.cookiebanners.enabled",
572     lazyStringSetPrefs: {},
573     illegalRegions: [],
574     showForEnterprise: true,
575   },
579  * Finish setting up the PromoInfo data structure by attaching lazy prefs getters
580  * as specified in the structure. (the object for each pref in the lazyStringSetPrefs
581  * gets a `lazyValue` property attached to it).
582  */
583 for (let promo of Object.values(PromoInfo)) {
584   for (let prefObj of Object.values(promo.lazyStringSetPrefs)) {
585     XPCOMUtils.defineLazyPreferenceGetter(
586       prefObj,
587       "lazyValue",
588       prefObj.name,
589       prefObj.default,
590       null,
591       stringPrefToSet
592     );
593   }
596 XPCOMUtils.defineLazyPreferenceGetter(
597   BrowserUtils,
598   "navigationRequireUserInteraction",
599   "browser.navigation.requireUserInteraction",
600   false
603 XPCOMUtils.defineLazyPreferenceGetter(
604   BrowserUtils,
605   "emailSupportedLocales",
606   "browser.send_to_device_locales",
607   "de,en-GB,en-US,es-AR,es-CL,es-ES,es-MX,fr,id,pl,pt-BR,ru,zh-TW",
608   null,
609   stringPrefToSet