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/. */
8 var EXPORTED_SYMBOLS = [ "BrowserUtils" ];
10 ChromeUtils.import("resource://gre/modules/Services.jsm");
11 ChromeUtils.defineModuleGetter(this, "PlacesUtils",
12 "resource://gre/modules/PlacesUtils.jsm");
17 * Prints arguments separated by a space and appends a new line.
26 * restartApplication: Restarts the application, keeping it in
27 * safe mode if it is already in safe mode.
29 restartApplication() {
30 let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"]
31 .createInstance(Ci.nsISupportsPRBool);
32 Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
33 if (cancelQuit.data) { // The quit request has been canceled.
36 // if already in safe mode restart in safe mode
37 if (Services.appinfo.inSafeMode) {
38 Services.startup.restartInSafeMode(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
41 Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
46 * urlSecurityCheck: JavaScript wrapper for checkLoadURIWithPrincipal
47 * and checkLoadURIStrWithPrincipal.
48 * If |aPrincipal| is not allowed to link to |aURL|, this function throws with
52 * The URL a page has linked to. This could be passed either as a string
53 * or as a nsIURI object.
55 * The principal of the document from which aURL came.
57 * Flags to be passed to checkLoadURIStr. If undefined,
58 * nsIScriptSecurityManager.STANDARD will be passed.
60 urlSecurityCheck(aURL, aPrincipal, aFlags) {
61 var secMan = Services.scriptSecurityManager;
62 if (aFlags === undefined) {
63 aFlags = secMan.STANDARD;
67 if (aURL instanceof Ci.nsIURI)
68 secMan.checkLoadURIWithPrincipal(aPrincipal, aURL, aFlags);
70 secMan.checkLoadURIStrWithPrincipal(aPrincipal, aURL, aFlags);
72 let principalStr = "";
74 principalStr = " from " + aPrincipal.URI.spec;
77 throw "Load of " + aURL + principalStr + " denied.";
82 * Return or create a principal with the codebase of one, and the originAttributes
83 * of an existing principal (e.g. on a docshell, where the originAttributes ought
84 * not to change, that is, we should keep the userContextId, privateBrowsingId,
85 * etc. the same when changing the principal).
88 * The principal whose codebase/null/system-ness we want.
89 * @param existingPrincipal
90 * The principal whose originAttributes we want, usually the current
91 * principal of a docshell.
92 * @return an nsIPrincipal that matches the codebase/null/system-ness of the first
93 * param, and the originAttributes of the second.
95 principalWithMatchingOA(principal, existingPrincipal) {
96 // Don't care about system principals:
97 if (principal.isSystemPrincipal) {
101 // If the originAttributes already match, just return the principal as-is.
102 if (existingPrincipal.originSuffix == principal.originSuffix) {
106 let secMan = Services.scriptSecurityManager;
107 if (principal.isCodebasePrincipal) {
108 return secMan.createCodebasePrincipal(principal.URI, existingPrincipal.originAttributes);
111 if (principal.isNullPrincipal) {
112 return secMan.createNullPrincipal(existingPrincipal.originAttributes);
114 throw new Error("Can't change the originAttributes of an expanded principal!");
118 * Constructs a new URI, using nsIIOService.
119 * @param aURL The URI spec.
120 * @param aOriginCharset The charset of the URI.
121 * @param aBaseURI Base URI to resolve aURL, or null.
122 * @return an nsIURI object based on aURL.
124 * @deprecated Use Services.io.newURI directly instead.
126 makeURI(aURL, aOriginCharset, aBaseURI) {
127 return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
131 * @deprecated Use Services.io.newFileURI directly instead.
134 return Services.io.newFileURI(aFile);
137 makeURIFromCPOW(aCPOWURI) {
138 return Services.io.newURI(aCPOWURI.spec);
142 * For a given DOM element, returns its position in "screen"
143 * coordinates. In a content process, the coordinates returned will
144 * be relative to the left/top of the tab. In the chrome process,
145 * the coordinates are relative to the user's screen.
147 getElementBoundingScreenRect(aElement) {
148 return this.getElementBoundingRect(aElement, true);
152 * For a given DOM element, returns its position as an offset from the topmost
153 * window. In a content process, the coordinates returned will be relative to
154 * the left/top of the topmost content area. If aInScreenCoords is true,
155 * screen coordinates will be returned instead.
157 getElementBoundingRect(aElement, aInScreenCoords) {
158 let rect = aElement.getBoundingClientRect();
159 let win = aElement.ownerGlobal;
161 let x = rect.left, y = rect.top;
163 // We need to compensate for any iframes that might shift things
164 // over. We also need to compensate for zooming.
165 let parentFrame = win.frameElement;
166 while (parentFrame) {
167 win = parentFrame.ownerGlobal;
168 let cstyle = win.getComputedStyle(parentFrame);
170 let framerect = parentFrame.getBoundingClientRect();
171 x += framerect.left + parseFloat(cstyle.borderLeftWidth) + parseFloat(cstyle.paddingLeft);
172 y += framerect.top + parseFloat(cstyle.borderTopWidth) + parseFloat(cstyle.paddingTop);
174 parentFrame = win.frameElement;
177 if (aInScreenCoords) {
178 x += win.mozInnerScreenX;
179 y += win.mozInnerScreenY;
182 let fullZoom = win.windowUtils.fullZoom;
186 width: rect.width * fullZoom,
187 height: rect.height * fullZoom,
193 onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab) {
194 // Don't modify non-default targets or targets that aren't in top-level app
195 // tab docshells (isAppTab will be false for app tab subframes).
196 if (originalTarget != "" || !isAppTab)
197 return originalTarget;
199 // External links from within app tabs should always open in new tabs
200 // instead of replacing the app tab's page (Bug 575561)
204 linkHost = linkURI.host;
205 docHost = linkNode.ownerDocument.documentURIObject.host;
207 // nsIURI.host can throw for non-nsStandardURL nsIURIs.
208 // If we fail to get either host, just return originalTarget.
209 return originalTarget;
212 if (docHost == linkHost)
213 return originalTarget;
215 // Special case: ignore "www" prefix if it is part of host string
216 let [longHost, shortHost] =
217 linkHost.length > docHost.length ? [linkHost, docHost] : [docHost, linkHost];
218 if (longHost == "www." + shortHost)
219 return originalTarget;
225 * Map the plugin's name to a filtered version more suitable for UI.
227 * @param aName The full-length name string of the plugin.
228 * @return the simplified name string.
230 makeNicePluginName(aName) {
231 if (aName == "Shockwave Flash")
232 return "Adobe Flash";
233 // Regex checks if aName begins with "Java" + non-letter char
234 if (/^Java\W/.exec(aName))
237 // Clean up the plugin name by stripping off parenthetical clauses,
238 // trailing version numbers or "plugin".
239 // EG, "Foo Bar (Linux) Plugin 1.23_02" --> "Foo Bar"
240 // Do this by first stripping the numbers, etc. off the end, and then
241 // removing "Plugin" (and then trimming to get rid of any whitespace).
242 // (Otherwise, something like "Java(TM) Plug-in 1.7.0_07" gets mangled)
243 let newName = aName.replace(/\(.*?\)/g, "").
244 replace(/[\s\d\.\-\_\(\)]+$/, "").
245 replace(/\bplug-?in\b/i, "").trim();
250 * Return true if linkNode has a rel="noreferrer" attribute.
252 * @param linkNode The <a> element, or null.
253 * @return a boolean indicating if linkNode has a rel="noreferrer" attribute.
255 linkHasNoReferrer(linkNode) {
256 // A null linkNode typically means that we're checking a link that wasn't
257 // provided via an <a> link, like a text-selected URL. Don't leak
258 // referrer information in this case.
262 let rel = linkNode.getAttribute("rel");
266 // The HTML spec says that rel should be split on spaces before looking
267 // for particular rel values.
268 let values = rel.split(/[ \t\r\n\f]/);
269 return values.includes("noreferrer");
273 * Returns true if |mimeType| is text-based, or false otherwise.
276 * The MIME type to check.
278 mimeTypeIsTextBased(mimeType) {
279 return mimeType.startsWith("text/") ||
280 mimeType.endsWith("+xml") ||
281 mimeType == "application/x-javascript" ||
282 mimeType == "application/javascript" ||
283 mimeType == "application/json" ||
284 mimeType == "application/xml" ||
285 mimeType == "mozilla.application/cached-xul";
289 * Return true if we should FAYT for this node + window (could be CPOW):
292 * The element that is focused
294 shouldFastFind(elt) {
296 let win = elt.ownerGlobal;
297 if (elt instanceof win.HTMLInputElement && elt.mozIsTextField(false))
300 if (elt.isContentEditable || win.document.designMode == "on")
303 if (elt instanceof win.HTMLTextAreaElement ||
304 elt instanceof win.HTMLSelectElement ||
305 elt instanceof win.HTMLObjectElement ||
306 elt instanceof win.HTMLEmbedElement)
314 * Returns true if we can show a find bar, including FAYT, for the specified
315 * document location. The location must not be in a blacklist of specific
316 * "about:" pages for which find is disabled.
318 * This can be called from the parent process or from content processes.
320 canFindInPage(location) {
321 return !location.startsWith("about:addons") &&
322 !location.startsWith("about:config") &&
323 !location.startsWith("about:preferences");
326 _visibleToolbarsMap: new WeakMap(),
329 * Return true if any or a specific toolbar that interacts with the content
330 * document is visible.
332 * @param {nsIDocShell} docShell The docShell instance that a toolbar should
333 * be interacting with
334 * @param {String} which Identifier of a specific toolbar
337 isToolbarVisible(docShell, which) {
338 let window = this.getRootWindow(docShell);
339 if (!this._visibleToolbarsMap.has(window))
341 let toolbars = this._visibleToolbarsMap.get(window);
342 return !!toolbars && toolbars.has(which);
346 * Sets the --toolbarbutton-button-height CSS property on the closest
347 * toolbar to the provided element. Useful if you need to vertically
348 * center a position:absolute element within a toolbar that uses
349 * -moz-pack-align:stretch, and thus a height which is dependant on
352 * @param element An element within the toolbar whose height is desired.
354 async setToolbarButtonHeightProperty(element) {
355 let window = element.ownerGlobal;
356 let dwu = window.windowUtils;
357 let toolbarItem = element;
358 let urlBarContainer = element.closest("#urlbar-container");
359 if (urlBarContainer) {
360 // The stop-reload-button, which is contained in #urlbar-container,
361 // needs to use #urlbar-container to calculate the bounds.
362 toolbarItem = urlBarContainer;
367 let bounds = dwu.getBoundsWithoutFlushing(toolbarItem);
368 if (!bounds.height) {
369 await window.promiseDocumentFlushed(() => {
370 bounds = dwu.getBoundsWithoutFlushing(toolbarItem);
374 toolbarItem.style.setProperty("--toolbarbutton-height", bounds.height + "px");
379 * Track whether a toolbar is visible for a given a docShell.
381 * @param {nsIDocShell} docShell The docShell instance that a toolbar should
382 * be interacting with
383 * @param {String} which Identifier of a specific toolbar
384 * @param {Boolean} [visible] Whether the toolbar is visible. Optional,
385 * defaults to `true`.
387 trackToolbarVisibility(docShell, which, visible = true) {
388 // We have to get the root window object, because XPConnect WrappedNatives
389 // can't be used as WeakMap keys.
390 let window = this.getRootWindow(docShell);
391 let toolbars = this._visibleToolbarsMap.get(window);
393 toolbars = new Set();
394 this._visibleToolbarsMap.set(window, toolbars);
397 toolbars.delete(which);
403 * Retrieve the root window object (i.e. the top-most content global) for a
404 * specific docShell object.
406 * @param {nsIDocShell} docShell
407 * @return {nsIDOMWindow}
409 getRootWindow(docShell) {
410 return docShell.sameTypeRootTreeItem.domWindow;
414 * Trim the selection text to a reasonable size and sanitize it to make it
415 * safe for search query input.
418 * The selection text to trim.
420 * The maximum string length, defaults to a reasonable size if undefined.
421 * @return The trimmed selection text.
423 trimSelection(aSelection, aMaxLen) {
424 // Selections of more than 150 characters aren't useful.
425 const maxLen = Math.min(aMaxLen || 150, aSelection.length);
427 if (aSelection.length > maxLen) {
428 // only use the first maxLen important chars. see bug 221361
429 let pattern = new RegExp("^(?:\\s*.){0," + maxLen + "}");
430 pattern.test(aSelection);
431 aSelection = RegExp.lastMatch;
434 aSelection = aSelection.trim().replace(/\s+/g, " ");
436 if (aSelection.length > maxLen) {
437 aSelection = aSelection.substr(0, maxLen);
444 * Retrieve the text selection details for the given window.
447 * The top window of the element containing the selection.
449 * The maximum string length for the selection text.
450 * @return The selection details containing the full and trimmed selection text
451 * and link details for link selections.
453 getSelectionDetails(aTopWindow, aCharLen) {
454 let focusedWindow = {};
455 let focusedElement = Services.focus.getFocusedElementForWindow(aTopWindow, true, focusedWindow);
456 focusedWindow = focusedWindow.value;
458 let selection = focusedWindow.getSelection();
459 let selectionStr = selection.toString();
465 // try getting a selected text in text input.
466 if (!selectionStr && focusedElement) {
467 // Don't get the selection for password fields. See bug 565717.
468 if (ChromeUtils.getClassName(focusedElement) === "HTMLTextAreaElement" ||
469 (ChromeUtils.getClassName(focusedElement) === "HTMLInputElement" &&
470 focusedElement.mozIsTextField(true))) {
471 selection = focusedElement.editor.selection;
472 selectionStr = selection.toString();
476 let collapsed = selection.isCollapsed;
479 // Have some text, let's figure out if it looks like a URL that isn't
481 linkText = selectionStr.trim();
482 if (/^(?:https?|ftp):/i.test(linkText)) {
484 url = this.makeURI(linkText);
486 } else if (/^(?:[a-z\d-]+\.)+[a-z]+$/i.test(linkText)) {
487 // Check if this could be a valid url, just missing the protocol.
488 // Now let's see if this is an intentional link selection. Our guess is
489 // based on whether the selection begins/ends with whitespace or is
490 // preceded/followed by a non-word character.
492 // selection.toString() trims trailing whitespace, so we look for
493 // that explicitly in the first and last ranges.
494 let beginRange = selection.getRangeAt(0);
495 let delimitedAtStart = /^\s/.test(beginRange);
496 if (!delimitedAtStart) {
497 let container = beginRange.startContainer;
498 let offset = beginRange.startOffset;
499 if (container.nodeType == container.TEXT_NODE && offset > 0)
500 delimitedAtStart = /\W/.test(container.textContent[offset - 1]);
502 delimitedAtStart = true;
505 let delimitedAtEnd = false;
506 if (delimitedAtStart) {
507 let endRange = selection.getRangeAt(selection.rangeCount - 1);
508 delimitedAtEnd = /\s$/.test(endRange);
509 if (!delimitedAtEnd) {
510 let container = endRange.endContainer;
511 let offset = endRange.endOffset;
512 if (container.nodeType == container.TEXT_NODE &&
513 offset < container.textContent.length)
514 delimitedAtEnd = /\W/.test(container.textContent[offset]);
516 delimitedAtEnd = true;
520 if (delimitedAtStart && delimitedAtEnd) {
522 url = Services.uriFixup.createFixupURI(linkText, Services.uriFixup.FIXUP_FLAG_NONE);
529 // Pass up to 16K through unmolested. If an add-on needs more, they will
530 // have to use a content script.
531 fullText = selectionStr.substr(0, 16384);
532 selectionStr = this.trimSelection(selectionStr, aCharLen);
535 if (url && !url.host) {
539 return { text: selectionStr, docSelectionIsCollapsed: collapsed, fullText,
540 linkURL: url ? url.spec : null, linkText: url ? linkText : "" };
543 // Iterates through every docshell in the window and calls PermitUnload.
544 canCloseWindow(window) {
545 let docShell = window.docShell;
546 for (let i = 0; i < docShell.childCount; ++i) {
547 let childShell = docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell);
548 let contentViewer = childShell.contentViewer;
549 if (contentViewer && !contentViewer.permitUnload()) {
558 * Replaces %s or %S in the provided url or postData with the given parameter,
559 * acccording to the best charset for the given url.
561 * @return [url, postData]
562 * @throws if nor url nor postData accept a param, but a param was provided.
564 async parseUrlAndPostData(url, postData, param) {
565 let hasGETParam = /%s/i.test(url);
566 let decodedPostData = postData ? unescape(postData) : "";
567 let hasPOSTParam = /%s/i.test(decodedPostData);
569 if (!hasGETParam && !hasPOSTParam) {
571 // If nor the url, nor postData contain parameters, but a parameter was
572 // provided, return the original input.
573 throw new Error("A param was provided but there's nothing to bind it to");
575 return [url, postData];
579 const re = /^(.*)\&mozcharset=([a-zA-Z][_\-a-zA-Z0-9]+)\s*$/;
580 let matches = url.match(re);
582 [, url, charset] = matches;
584 // Try to fetch a charset from History.
586 // Will return an empty string if character-set is not found.
587 let pageInfo = await PlacesUtils.history.fetch(url, {includeAnnotations: true});
588 if (pageInfo && pageInfo.annotations.has(PlacesUtils.CHARSET_ANNO)) {
589 charset = pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO);
592 // makeURI() throws if url is invalid.
597 // encodeURIComponent produces UTF-8, and cannot be used for other charsets.
598 // escape() works in those cases, but it doesn't uri-encode +, @, and /.
599 // Therefore we need to manually replace these ASCII characters by their
600 // encodeURIComponent result, to match the behavior of nsEscape() with
602 let encodedParam = "";
603 if (charset && charset != "UTF-8") {
605 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
606 .createInstance(Ci.nsIScriptableUnicodeConverter);
607 converter.charset = charset;
608 encodedParam = converter.ConvertFromUnicode(param) + converter.Finish();
610 encodedParam = param;
612 encodedParam = escape(encodedParam).replace(/[+@\/]+/g, encodeURIComponent);
614 // Default charset is UTF-8
615 encodedParam = encodeURIComponent(param);
618 url = url.replace(/%s/g, encodedParam).replace(/%S/g, param);
620 postData = decodedPostData.replace(/%s/g, encodedParam)
621 .replace(/%S/g, param);
623 return [url, postData];
627 * Generate a document fragment for a localized string that has DOM
628 * node replacements. This avoids using getFormattedString followed
629 * by assigning to innerHTML. Fluent can probably replace this when
630 * it is in use everywhere.
632 * @param {Document} doc
633 * @param {String} msg
634 * The string to put replacements in. Fetch from
635 * a stringbundle using getString or GetStringFromName,
636 * or even an inserted dtd string.
637 * @param {Node|String} nodesOrStrings
638 * The replacement items. Can be a mix of Nodes
639 * and Strings. However, for correct behaviour, the
640 * number of items provided needs to exactly match
641 * the number of replacement strings in the l10n string.
642 * @returns {DocumentFragment}
643 * A document fragment. In the trivial case (no
644 * replacements), this will simply be a fragment with 1
645 * child, a text node containing the localized string.
647 getLocalizedFragment(doc, msg, ...nodesOrStrings) {
648 // Ensure replacement points are indexed:
649 for (let i = 1; i <= nodesOrStrings.length; i++) {
650 if (!msg.includes("%" + i + "$S")) {
651 msg = msg.replace(/%S/, "%" + i + "$S");
654 let numberOfInsertionPoints = msg.match(/%\d+\$S/g).length;
655 if (numberOfInsertionPoints != nodesOrStrings.length) {
656 Cu.reportError(`Message has ${numberOfInsertionPoints} insertion points, ` +
657 `but got ${nodesOrStrings.length} replacement parameters!`);
660 let fragment = doc.createDocumentFragment();
662 let insertionPoint = 1;
663 for (let replacement of nodesOrStrings) {
664 let insertionString = "%" + (insertionPoint++) + "$S";
665 let partIndex = parts.findIndex(part => typeof part == "string" && part.includes(insertionString));
666 if (partIndex == -1) {
667 fragment.appendChild(doc.createTextNode(msg));
671 if (typeof replacement == "string") {
672 parts[partIndex] = parts[partIndex].replace(insertionString, replacement);
674 let [firstBit, lastBit] = parts[partIndex].split(insertionString);
675 parts.splice(partIndex, 1, firstBit, replacement, lastBit);
679 // Put everything in a document fragment:
680 for (let part of parts) {
681 if (typeof part == "string") {
683 fragment.appendChild(doc.createTextNode(part));
686 fragment.appendChild(part);