Backed out changeset 496886cb30a5 (bug 1867152) for bc failures on browser_user_input...
[gecko.git] / browser / modules / BrowserUIUtils.sys.mjs
blob98e81c6d559331cdf937cb607bd2e57e62f49921
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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
8 const lazy = {};
10 XPCOMUtils.defineLazyPreferenceGetter(
11   lazy,
12   "trimHttps",
13   "browser.urlbar.trimHttps"
15 export var BrowserUIUtils = {
16   /**
17    * Check whether a page can be considered as 'empty', that its URI
18    * reflects its origin, and that if it's loaded in a tab, that tab
19    * could be considered 'empty' (e.g. like the result of opening
20    * a 'blank' new tab).
21    *
22    * We have to do more than just check the URI, because especially
23    * for things like about:blank, it is possible that the opener or
24    * some other page has control over the contents of the page.
25    *
26    * @param {Browser} browser
27    *        The browser whose page we're checking.
28    * @param {nsIURI} [uri]
29    *        The URI against which we're checking (the browser's currentURI
30    *        if omitted).
31    *
32    * @return {boolean} false if the page was opened by or is controlled by
33    *         arbitrary web content, unless that content corresponds with the URI.
34    *         true if the page is blank and controlled by a principal matching
35    *         that URI (or the system principal if the principal has no URI)
36    */
37   checkEmptyPageOrigin(browser, uri = browser.currentURI) {
38     // If another page opened this page with e.g. window.open, this page might
39     // be controlled by its opener.
40     if (browser.hasContentOpener) {
41       return false;
42     }
43     let contentPrincipal = browser.contentPrincipal;
44     // Not all principals have URIs...
45     // There are two special-cases involving about:blank. One is where
46     // the user has manually loaded it and it got created with a null
47     // principal. The other involves the case where we load
48     // some other empty page in a browser and the current page is the
49     // initial about:blank page (which has that as its principal, not
50     // just URI in which case it could be web-based). Especially in
51     // e10s, we need to tackle that case specifically to avoid race
52     // conditions when updating the URL bar.
53     //
54     // Note that we check the documentURI here, since the currentURI on
55     // the browser might have been set by SessionStore in order to
56     // support switch-to-tab without having actually loaded the content
57     // yet.
58     let uriToCheck = browser.documentURI || uri;
59     if (
60       (uriToCheck.spec == "about:blank" && contentPrincipal.isNullPrincipal) ||
61       contentPrincipal.spec == "about:blank"
62     ) {
63       return true;
64     }
65     if (contentPrincipal.isContentPrincipal) {
66       return contentPrincipal.equalsURI(uri);
67     }
68     // ... so for those that don't have them, enforce that the page has the
69     // system principal (this matches e.g. on about:newtab).
70     return contentPrincipal.isSystemPrincipal;
71   },
73   /**
74    * Generate a document fragment for a localized string that has DOM
75    * node replacements. This avoids using getFormattedString followed
76    * by assigning to innerHTML. Fluent can probably replace this when
77    * it is in use everywhere.
78    *
79    * @param {Document} doc
80    * @param {String}   msg
81    *                   The string to put replacements in. Fetch from
82    *                   a stringbundle using getString or GetStringFromName,
83    *                   or even an inserted dtd string.
84    * @param {Node|String} nodesOrStrings
85    *                   The replacement items. Can be a mix of Nodes
86    *                   and Strings. However, for correct behaviour, the
87    *                   number of items provided needs to exactly match
88    *                   the number of replacement strings in the l10n string.
89    * @returns {DocumentFragment}
90    *                   A document fragment. In the trivial case (no
91    *                   replacements), this will simply be a fragment with 1
92    *                   child, a text node containing the localized string.
93    */
94   getLocalizedFragment(doc, msg, ...nodesOrStrings) {
95     // Ensure replacement points are indexed:
96     for (let i = 1; i <= nodesOrStrings.length; i++) {
97       if (!msg.includes("%" + i + "$S")) {
98         msg = msg.replace(/%S/, "%" + i + "$S");
99       }
100     }
101     let numberOfInsertionPoints = msg.match(/%\d+\$S/g).length;
102     if (numberOfInsertionPoints != nodesOrStrings.length) {
103       console.error(
104         `Message has ${numberOfInsertionPoints} insertion points, ` +
105           `but got ${nodesOrStrings.length} replacement parameters!`
106       );
107     }
109     let fragment = doc.createDocumentFragment();
110     let parts = [msg];
111     let insertionPoint = 1;
112     for (let replacement of nodesOrStrings) {
113       let insertionString = "%" + insertionPoint++ + "$S";
114       let partIndex = parts.findIndex(
115         part => typeof part == "string" && part.includes(insertionString)
116       );
117       if (partIndex == -1) {
118         fragment.appendChild(doc.createTextNode(msg));
119         return fragment;
120       }
122       if (typeof replacement == "string") {
123         parts[partIndex] = parts[partIndex].replace(
124           insertionString,
125           replacement
126         );
127       } else {
128         let [firstBit, lastBit] = parts[partIndex].split(insertionString);
129         parts.splice(partIndex, 1, firstBit, replacement, lastBit);
130       }
131     }
133     // Put everything in a document fragment:
134     for (let part of parts) {
135       if (typeof part == "string") {
136         if (part) {
137           fragment.appendChild(doc.createTextNode(part));
138         }
139       } else {
140         fragment.appendChild(part);
141       }
142     }
143     return fragment;
144   },
146   removeSingleTrailingSlashFromURL(aURL) {
147     // remove single trailing slash for http/https/ftp URLs
148     return aURL.replace(/^((?:http|https|ftp):\/\/[^/]+)\/$/, "$1");
149   },
151   get trimURLProtocol() {
152     return lazy.trimHttps ? "https://" : "http://";
153   },
155   /**
156    * Returns a URL which has been trimmed by removing 'http://' or 'https://',
157    * when the pref 'trimHttps' is set to true, and any trailing slash
158    * (in http/https/ftp urls). Note that a trimmed url may not load the same
159    * page as the original url, so before loading it, it must be passed through
160    * URIFixup, to check trimming doesn't change its destination. We don't run
161    * the URIFixup check here, because trimURL is in the page load path
162    * (see onLocationChange), so it must be fast and simple.
163    *
164    * @param {string} aURL The URL to trim.
165    * @returns {string} The trimmed string.
166    */
167   trimURL(aURL) {
168     let url = this.removeSingleTrailingSlashFromURL(aURL);
169     return url.startsWith(this.trimURLProtocol)
170       ? url.substring(this.trimURLProtocol.length)
171       : url;
172   },
175 XPCOMUtils.defineLazyPreferenceGetter(
176   BrowserUIUtils,
177   "quitShortcutDisabled",
178   "browser.quitShortcut.disabled",
179   false