Bug 1888590 - Mark some subtests on trusted-types-event-handlers.html as failing...
[gecko.git] / toolkit / modules / BrowserUtils.sys.mjs
blobdb9ef425c5f52890309a94fe27a5036b6b1cf593
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   }
26 XPCOMUtils.defineLazyPreferenceGetter(
27   lazy,
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);
34 });
36 function stringPrefToSet(prefVal) {
37   return new Set(
38     prefVal
39       .toLowerCase()
40       .split(/\s*,\s*/g) // split on commas, ignoring whitespace
41       .filter(v => !!v) // discard any falsey values
42   );
45 export var BrowserUtils = {
46   /**
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).
51    *
52    * @param 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.
59    */
60   principalWithMatchingOA(principal, existingPrincipal) {
61     // Don't care about system principals:
62     if (principal.isSystemPrincipal) {
63       return principal;
64     }
66     // If the originAttributes already match, just return the principal as-is.
67     if (existingPrincipal.originSuffix == principal.originSuffix) {
68       return principal;
69     }
71     let secMan = Services.scriptSecurityManager;
72     if (principal.isContentPrincipal) {
73       return secMan.principalWithOA(
74         principal,
75         existingPrincipal.originAttributes
76       );
77     }
79     if (principal.isNullPrincipal) {
80       return secMan.createNullPrincipal(existingPrincipal.originAttributes);
81     }
82     throw new Error(
83       "Can't change the originAttributes of an expanded principal!"
84     );
85   },
87   /**
88    * Returns true if |mimeType| is text-based, or false otherwise.
89    *
90    * @param mimeType
91    *        The MIME type to check.
92    */
93   mimeTypeIsTextBased(mimeType) {
94     return (
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"
102     );
103   },
105   /**
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.
109    *
110    * This can be called from the parent process or from content processes.
111    */
112   canFindInPage(location) {
113     return (
114       !location.startsWith("about:preferences") &&
115       !location.startsWith("about:settings") &&
116       !location.startsWith("about:logins") &&
117       !(location.startsWith("about:firefoxview") && lazy.FXVIEW_SEARCH_ENABLED)
118     );
119   },
121   isFindbarVisible(docShell) {
122     const FINDER_SYS_MJS = "resource://gre/modules/Finder.sys.mjs";
123     return (
124       Cu.isESModuleLoaded(FINDER_SYS_MJS) &&
125       ChromeUtils.importESModule(FINDER_SYS_MJS).Finder.isFindbarVisible(
126         docShell
127       )
128     );
129   },
131   /**
132    * Returns a Promise which resolves when the given observer topic has been
133    * observed.
134    *
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>}
142    */
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 });
149         }
150       };
151       Services.obs.addObserver(observer, topic);
152     });
153   },
155   formatURIStringForDisplay(uriString, options = {}) {
156     try {
157       return this.formatURIForDisplay(Services.io.newURI(uriString), options);
158     } catch (ex) {
159       return uriString;
160     }
161   },
163   formatURIForDisplay(uri, options = {}) {
164     let { showInsecureHTTP = false } = options;
165     switch (uri.scheme) {
166       case "view-source":
167         let innerURI = uri.spec.substring("view-source:".length);
168         return this.formatURIStringForDisplay(innerURI, options);
169       case "http":
170       // Fall through.
171       case "https":
172         let host = uri.displayHostPort;
173         if (!showInsecureHTTP && host.startsWith("www.")) {
174           host = Services.eTLD.getSchemelessSite(uri);
175         }
176         if (showInsecureHTTP && uri.scheme == "http") {
177           return "http://" + host;
178         }
179         return host;
180       case "about":
181         return "about:" + uri.filePath;
182       case "blob":
183         try {
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);
188           }
189           // otherwise, fall through...
190         } catch (ex) {
191           console.error("Invalid blob URI passed to formatURIForDisplay: ", ex);
192         }
193       /* For blob URIs without an origin, fall through and use the data URI
194        * logic (shows just "(data)", localized). */
195       case "data":
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 }
202         );
203       case "chrome":
204       case "resource":
205       case "jar":
206       case "file":
207       default:
208         try {
209           let url = uri.QueryInterface(Ci.nsIURL);
210           // Just the filename if we have one:
211           if (url.fileName) {
212             return url.fileName;
213           }
214           // We won't get a filename for a path that looks like:
215           // /foo/bar/baz/
216           // So try the directory name:
217           if (url.directory) {
218             let parts = url.directory.split("/");
219             // Pop off any empty bits at the end:
220             let last;
221             while (!last && parts.length) {
222               last = parts.pop();
223             }
224             if (last) {
225               return last;
226             }
227           }
228         } catch (ex) {
229           console.error(ex);
230         }
231     }
232     return uri.asciiHost || uri.spec;
233   },
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) {
238     if (!url) {
239       return null;
240     }
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));
245     }
246     // Disallow sharing URLs with more than 65535 characters.
247     if (url.spec.length > 65535) {
248       return null;
249     }
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;
253   },
255   /**
256    * Extracts linkNode and href for a click event.
257    *
258    * @param event
259    *        The click event.
260    * @return [href, linkNode, linkPrincipal].
261    *
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.
265    */
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) {
270       return null;
271     }
272     function isHTMLLink(aNode) {
273       // Be consistent with what nsContextMenu.js does.
274       return (
275         (content.HTMLAnchorElement.isInstance(aNode) && aNode.href) ||
276         (content.HTMLAreaElement.isInstance(aNode) && aNode.href) ||
277         content.HTMLLinkElement.isInstance(aNode)
278       );
279     }
281     let node = event.composedTarget;
282     while (node && !isHTMLLink(node)) {
283       node = node.flattenedTreeParentNode;
284     }
286     if (node) {
287       return [node.href, node, node.ownerDocument.nodePrincipal];
288     }
290     // If there is no linkNode, try simple XLink.
291     let href, baseURI;
292     node = event.composedTarget;
293     while (node && !href) {
294       if (
295         node.nodeType == content.Node.ELEMENT_NODE &&
296         (node.localName == "a" ||
297           node.namespaceURI == "http://www.w3.org/1998/Math/MathML")
298       ) {
299         href =
300           node.getAttribute("href") ||
301           node.getAttributeNS("http://www.w3.org/1999/xlink", "href");
302         if (href) {
303           baseURI = node.ownerDocument.baseURIObject;
304           break;
305         }
306       }
307       node = node.flattenedTreeParentNode;
308     }
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.
313     return [
314       href ? Services.io.newURI(href, null, baseURI).spec : null,
315       null,
316       node && node.ownerDocument.nodePrincipal,
317     ];
318   },
320   /**
321    * whereToOpenLink() looks at an event to decide where to open a link.
322    *
323    * The event may be a mouse event (click, double-click, middle-click) or keypress event (enter).
324    *
325    * On Windows, the modifiers are:
326    * Ctrl        new tab, selected
327    * Shift       new window
328    * Ctrl+Shift  new tab, in background
329    * Alt         save
330    *
331    * Middle-clicking is the same as Ctrl+clicking (it opens a new tab).
332    *
333    * Exceptions:
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.
339    *
340    * @param e {Event|Object} Event or JSON Object
341    * @param ignoreButton {Boolean}
342    * @param ignoreAlt {Boolean}
343    * @returns {"current" | "tabshifted" | "tab" | "save" | "window"}
344    */
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.
349     if (!e) {
350       return "current";
351     }
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",
364       true
365     );
366     let middleUsesNewWindow = Services.prefs.getBoolPref(
367       "middlemouse.openNewWindow",
368       false
369     );
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";
379     }
381     if (alt && Services.prefs.getBoolPref("browser.altClickSave", false)) {
382       return "save";
383     }
385     if (shift || (middle && !middleUsesTabs && middleUsesNewWindow)) {
386       return "window";
387     }
389     return "current";
390   },
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.
398     if (!aEvent) {
399       return aEvent;
400     }
401     let tempEvent = aEvent;
402     while (tempEvent.sourceEvent) {
403       if (tempEvent.sourceEvent.button == 1) {
404         aEvent = tempEvent.sourceEvent;
405         break;
406       }
407       tempEvent = tempEvent.sourceEvent;
408     }
409     return aEvent;
410   },
412   /**
413    * An enumeration of the promotion types that can be passed to shouldShowPromo
414    */
415   PromoType: {
416     DEFAULT: 0, // invalid
417     VPN: 1,
418     RELAY: 2,
419     FOCUS: 3,
420     PIN: 4,
421     COOKIE_BANNERS: 5,
422   },
424   /**
425    * Should a given promo be shown to the user now, based on things including:
426    *
427    *  current region
428    *  home region
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
434    *
435    * @param {BrowserUtils.PromoType} promoType - What promo are we checking on?
436    *
437    * @return {boolean} - should we display this promo now or not?
438    */
439   shouldShowPromo(promoType) {
440     switch (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:
446         break;
447       default:
448         throw new Error("Unknown promo type: ", promoType);
449     }
451     const info = PromoInfo[promoType];
452     const promoEnabled =
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;
462       inSupportedRegion =
463         supportedRegions.has(currentRegion.toLowerCase()) ||
464         supportedRegions.has(homeRegion.toLowerCase());
465     }
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();
479     return (
480       promoEnabled &&
481       !avoidAdsRegions?.has(homeRegion.toLowerCase()) &&
482       !avoidAdsRegions?.has(currentRegion.toLowerCase()) &&
483       !info.illegalRegions.includes(homeRegion.toLowerCase()) &&
484       !info.illegalRegions.includes(currentRegion.toLowerCase()) &&
485       inSupportedRegion &&
486       noActivePolicy &&
487       passedExtraCheck
488     );
489   },
491   /**
492    * @deprecated in favor of shouldShowPromo
493    */
494   shouldShowVPNPromo() {
495     return this.shouldShowPromo(this.PromoType.VPN);
496   },
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);
502   },
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.
509  */
510 let PromoInfo = {
511   [BrowserUtils.PromoType.VPN]: {
512     enabledPref: "browser.vpn_promo.enabled",
513     lazyStringSetPrefs: {
514       supportedRegions: {
515         name: "browser.contentblocking.report.vpn_regions",
516         default:
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",
518       },
519       disallowedRegions: {
520         name: "browser.vpn_promo.disallowed_regions",
521         default: "ae,by,cn,cu,iq,ir,kp,om,ru,sd,sy,tm,tr",
522       },
523     },
524     //See https://github.com/search?q=repo%3Amozilla%2Fbedrock+VPN_EXCLUDED_COUNTRY_CODES&type=code
525     illegalRegions: [
526       "ae",
527       "by",
528       "cn",
529       "cu",
530       "iq",
531       "ir",
532       "kp",
533       "om",
534       "ru",
535       "sd",
536       "sy",
537       "tm",
538       "tr",
539     ],
540   },
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
546       disallowedRegions: {
547         name: "browser.promo.focus.disallowed_regions",
548         default: "cn",
549       },
550     },
551     illegalRegions: ["cn"],
552   },
553   [BrowserUtils.PromoType.PIN]: {
554     enabledPref: "browser.promo.pin.enabled",
555     lazyStringSetPrefs: {},
556     illegalRegions: [],
557   },
558   [BrowserUtils.PromoType.RELAY]: {
559     lazyStringSetPrefs: {},
560     illegalRegions: [],
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.
564     extraCheck: () =>
565       !Services.prefs.getCharPref("identity.fxaccounts.autoconfig.uri", "") &&
566       [
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)),
574   },
575   [BrowserUtils.PromoType.COOKIE_BANNERS]: {
576     enabledPref: "browser.promo.cookiebanners.enabled",
577     lazyStringSetPrefs: {},
578     illegalRegions: [],
579     showForEnterprise: true,
580   },
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).
587  */
588 for (let promo of Object.values(PromoInfo)) {
589   for (let prefObj of Object.values(promo.lazyStringSetPrefs)) {
590     XPCOMUtils.defineLazyPreferenceGetter(
591       prefObj,
592       "lazyValue",
593       prefObj.name,
594       prefObj.default,
595       null,
596       stringPrefToSet
597     );
598   }
601 XPCOMUtils.defineLazyPreferenceGetter(
602   BrowserUtils,
603   "navigationRequireUserInteraction",
604   "browser.navigation.requireUserInteraction",
605   false
608 XPCOMUtils.defineLazyPreferenceGetter(
609   BrowserUtils,
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",
613   null,
614   stringPrefToSet