summaryrefslogtreecommitdiffstats
path: root/browser/modules/BrowserUIUtils.jsm
blob: 9124b0b31b987a65c26c5762f761036dd45b1202 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
/* -*- 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/. */

"use strict";

var { XPCOMUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/XPCOMUtils.sys.mjs"
);

var EXPORTED_SYMBOLS = ["BrowserUIUtils"];

var BrowserUIUtils = {
  /**
   * Check whether a page can be considered as 'empty', that its URI
   * reflects its origin, and that if it's loaded in a tab, that tab
   * could be considered 'empty' (e.g. like the result of opening
   * a 'blank' new tab).
   *
   * We have to do more than just check the URI, because especially
   * for things like about:blank, it is possible that the opener or
   * some other page has control over the contents of the page.
   *
   * @param {Browser} browser
   *        The browser whose page we're checking.
   * @param {nsIURI} [uri]
   *        The URI against which we're checking (the browser's currentURI
   *        if omitted).
   *
   * @return {boolean} false if the page was opened by or is controlled by
   *         arbitrary web content, unless that content corresponds with the URI.
   *         true if the page is blank and controlled by a principal matching
   *         that URI (or the system principal if the principal has no URI)
   */
  checkEmptyPageOrigin(browser, uri = browser.currentURI) {
    // If another page opened this page with e.g. window.open, this page might
    // be controlled by its opener.
    if (browser.hasContentOpener) {
      return false;
    }
    let contentPrincipal = browser.contentPrincipal;
    // Not all principals have URIs...
    // There are two special-cases involving about:blank. One is where
    // the user has manually loaded it and it got created with a null
    // principal. The other involves the case where we load
    // some other empty page in a browser and the current page is the
    // initial about:blank page (which has that as its principal, not
    // just URI in which case it could be web-based). Especially in
    // e10s, we need to tackle that case specifically to avoid race
    // conditions when updating the URL bar.
    //
    // Note that we check the documentURI here, since the currentURI on
    // the browser might have been set by SessionStore in order to
    // support switch-to-tab without having actually loaded the content
    // yet.
    let uriToCheck = browser.documentURI || uri;
    if (
      (uriToCheck.spec == "about:blank" && contentPrincipal.isNullPrincipal) ||
      contentPrincipal.spec == "about:blank"
    ) {
      return true;
    }
    if (contentPrincipal.isContentPrincipal) {
      return contentPrincipal.equalsURI(uri);
    }
    // ... so for those that don't have them, enforce that the page has the
    // system principal (this matches e.g. on about:newtab).
    return contentPrincipal.isSystemPrincipal;
  },

  /**
   * Sets the --toolbarbutton-button-height CSS property on the closest
   * toolbar to the provided element. Useful if you need to vertically
   * center a position:absolute element within a toolbar that uses
   * -moz-pack-align:stretch, and thus a height which is dependant on
   * the font-size.
   *
   * @param element An element within the toolbar whose height is desired.
   */
  async setToolbarButtonHeightProperty(element) {
    let window = element.ownerGlobal;
    let dwu = window.windowUtils;
    let toolbarItem = element;
    let urlBarContainer = element.closest("#urlbar-container");
    if (urlBarContainer) {
      // The stop-reload-button, which is contained in #urlbar-container,
      // needs to use #urlbar-container to calculate the bounds.
      toolbarItem = urlBarContainer;
    }
    if (!toolbarItem) {
      return;
    }
    let bounds = dwu.getBoundsWithoutFlushing(toolbarItem);
    if (!bounds.height) {
      await window.promiseDocumentFlushed(() => {
        bounds = dwu.getBoundsWithoutFlushing(toolbarItem);
      });
    }
    if (bounds.height) {
      toolbarItem.style.setProperty(
        "--toolbarbutton-height",
        bounds.height + "px"
      );
    }
  },

  /**
   * Generate a document fragment for a localized string that has DOM
   * node replacements. This avoids using getFormattedString followed
   * by assigning to innerHTML. Fluent can probably replace this when
   * it is in use everywhere.
   *
   * @param {Document} doc
   * @param {String}   msg
   *                   The string to put replacements in. Fetch from
   *                   a stringbundle using getString or GetStringFromName,
   *                   or even an inserted dtd string.
   * @param {Node|String} nodesOrStrings
   *                   The replacement items. Can be a mix of Nodes
   *                   and Strings. However, for correct behaviour, the
   *                   number of items provided needs to exactly match
   *                   the number of replacement strings in the l10n string.
   * @returns {DocumentFragment}
   *                   A document fragment. In the trivial case (no
   *                   replacements), this will simply be a fragment with 1
   *                   child, a text node containing the localized string.
   */
  getLocalizedFragment(doc, msg, ...nodesOrStrings) {
    // Ensure replacement points are indexed:
    for (let i = 1; i <= nodesOrStrings.length; i++) {
      if (!msg.includes("%" + i + "$S")) {
        msg = msg.replace(/%S/, "%" + i + "$S");
      }
    }
    let numberOfInsertionPoints = msg.match(/%\d+\$S/g).length;
    if (numberOfInsertionPoints != nodesOrStrings.length) {
      console.error(
        `Message has ${numberOfInsertionPoints} insertion points, ` +
          `but got ${nodesOrStrings.length} replacement parameters!`
      );
    }

    let fragment = doc.createDocumentFragment();
    let parts = [msg];
    let insertionPoint = 1;
    for (let replacement of nodesOrStrings) {
      let insertionString = "%" + insertionPoint++ + "$S";
      let partIndex = parts.findIndex(
        part => typeof part == "string" && part.includes(insertionString)
      );
      if (partIndex == -1) {
        fragment.appendChild(doc.createTextNode(msg));
        return fragment;
      }

      if (typeof replacement == "string") {
        parts[partIndex] = parts[partIndex].replace(
          insertionString,
          replacement
        );
      } else {
        let [firstBit, lastBit] = parts[partIndex].split(insertionString);
        parts.splice(partIndex, 1, firstBit, replacement, lastBit);
      }
    }

    // Put everything in a document fragment:
    for (let part of parts) {
      if (typeof part == "string") {
        if (part) {
          fragment.appendChild(doc.createTextNode(part));
        }
      } else {
        fragment.appendChild(part);
      }
    }
    return fragment;
  },

  removeSingleTrailingSlashFromURL(aURL) {
    // remove single trailing slash for http/https/ftp URLs
    return aURL.replace(/^((?:http|https|ftp):\/\/[^/]+)\/$/, "$1");
  },

  /**
   * Returns a URL which has been trimmed by removing 'http://' and any
   * trailing slash (in http/https/ftp urls).
   * Note that a trimmed url may not load the same page as the original url, so
   * before loading it, it must be passed through URIFixup, to check trimming
   * doesn't change its destination. We don't run the URIFixup check here,
   * because trimURL is in the page load path (see onLocationChange), so it
   * must be fast and simple.
   *
   * @param {string} aURL The URL to trim.
   * @returns {string} The trimmed string.
   */
  get trimURLProtocol() {
    return "http://";
  },
  trimURL(aURL) {
    let url = this.removeSingleTrailingSlashFromURL(aURL);
    // Remove "http://" prefix.
    return url.startsWith(this.trimURLProtocol)
      ? url.substring(this.trimURLProtocol.length)
      : url;
  },
};

XPCOMUtils.defineLazyPreferenceGetter(
  BrowserUIUtils,
  "quitShortcutDisabled",
  "browser.quitShortcut.disabled",
  false
);