diff options
Diffstat (limited to 'toolkit/modules/BrowserUtils.jsm')
-rw-r--r-- | toolkit/modules/BrowserUtils.jsm | 586 |
1 files changed, 586 insertions, 0 deletions
diff --git a/toolkit/modules/BrowserUtils.jsm b/toolkit/modules/BrowserUtils.jsm new file mode 100644 index 000000000..862f9619c --- /dev/null +++ b/toolkit/modules/BrowserUtils.jsm @@ -0,0 +1,586 @@ +/* -*- 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"; + +this.EXPORTED_SYMBOLS = [ "BrowserUtils" ]; + +const {interfaces: Ci, utils: Cu, classes: Cc} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); + +Cu.importGlobalProperties(['URL']); + +this.BrowserUtils = { + + /** + * Prints arguments separated by a space and appends a new line. + */ + dumpLn: function (...args) { + for (let a of args) + dump(a + " "); + dump("\n"); + }, + + /** + * restartApplication: Restarts the application, keeping it in + * safe mode if it is already in safe mode. + */ + restartApplication: function() { + let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"] + .getService(Ci.nsIAppStartup); + let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"] + .createInstance(Ci.nsISupportsPRBool); + Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart"); + if (cancelQuit.data) { // The quit request has been canceled. + return false; + } + // if already in safe mode restart in safe mode + if (Services.appinfo.inSafeMode) { + appStartup.restartInSafeMode(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart); + return undefined; + } + appStartup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart); + return undefined; + }, + + /** + * urlSecurityCheck: JavaScript wrapper for checkLoadURIWithPrincipal + * and checkLoadURIStrWithPrincipal. + * If |aPrincipal| is not allowed to link to |aURL|, this function throws with + * an error message. + * + * @param aURL + * The URL a page has linked to. This could be passed either as a string + * or as a nsIURI object. + * @param aPrincipal + * The principal of the document from which aURL came. + * @param aFlags + * Flags to be passed to checkLoadURIStr. If undefined, + * nsIScriptSecurityManager.STANDARD will be passed. + */ + urlSecurityCheck: function(aURL, aPrincipal, aFlags) { + var secMan = Services.scriptSecurityManager; + if (aFlags === undefined) { + aFlags = secMan.STANDARD; + } + + try { + if (aURL instanceof Ci.nsIURI) + secMan.checkLoadURIWithPrincipal(aPrincipal, aURL, aFlags); + else + secMan.checkLoadURIStrWithPrincipal(aPrincipal, aURL, aFlags); + } catch (e) { + let principalStr = ""; + try { + principalStr = " from " + aPrincipal.URI.spec; + } + catch (e2) { } + + throw "Load of " + aURL + principalStr + " denied."; + } + }, + + /** + * Return or create a principal with the codebase of one, and the originAttributes + * of an existing principal (e.g. on a docshell, where the originAttributes ought + * not to change, that is, we should keep the userContextId, privateBrowsingId, + * etc. the same when changing the principal). + * + * @param principal + * The principal whose codebase/null/system-ness we want. + * @param existingPrincipal + * The principal whose originAttributes we want, usually the current + * principal of a docshell. + * @return an nsIPrincipal that matches the codebase/null/system-ness of the first + * param, and the originAttributes of the second. + */ + principalWithMatchingOA(principal, existingPrincipal) { + // Don't care about system principals: + if (principal.isSystemPrincipal) { + return principal; + } + + // If the originAttributes already match, just return the principal as-is. + if (existingPrincipal.originSuffix == principal.originSuffix) { + return principal; + } + + let secMan = Services.scriptSecurityManager; + if (principal.isCodebasePrincipal) { + return secMan.createCodebasePrincipal(principal.URI, existingPrincipal.originAttributes); + } + + if (principal.isNullPrincipal) { + return secMan.createNullPrincipal(existingPrincipal.originAttributes); + } + throw new Error("Can't change the originAttributes of an expanded principal!"); + }, + + /** + * Constructs a new URI, using nsIIOService. + * @param aURL The URI spec. + * @param aOriginCharset The charset of the URI. + * @param aBaseURI Base URI to resolve aURL, or null. + * @return an nsIURI object based on aURL. + */ + makeURI: function(aURL, aOriginCharset, aBaseURI) { + return Services.io.newURI(aURL, aOriginCharset, aBaseURI); + }, + + makeFileURI: function(aFile) { + return Services.io.newFileURI(aFile); + }, + + makeURIFromCPOW: function(aCPOWURI) { + return Services.io.newURI(aCPOWURI.spec, aCPOWURI.originCharset, null); + }, + + /** + * For a given DOM element, returns its position in "screen" + * coordinates. In a content process, the coordinates returned will + * be relative to the left/top of the tab. In the chrome process, + * the coordinates are relative to the user's screen. + */ + getElementBoundingScreenRect: function(aElement) { + return this.getElementBoundingRect(aElement, true); + }, + + /** + * For a given DOM element, returns its position as an offset from the topmost + * window. In a content process, the coordinates returned will be relative to + * the left/top of the topmost content area. If aInScreenCoords is true, + * screen coordinates will be returned instead. + */ + getElementBoundingRect: function(aElement, aInScreenCoords) { + let rect = aElement.getBoundingClientRect(); + let win = aElement.ownerDocument.defaultView; + + let x = rect.left, y = rect.top; + + // We need to compensate for any iframes that might shift things + // over. We also need to compensate for zooming. + let parentFrame = win.frameElement; + while (parentFrame) { + win = parentFrame.ownerDocument.defaultView; + let cstyle = win.getComputedStyle(parentFrame, ""); + + let framerect = parentFrame.getBoundingClientRect(); + x += framerect.left + parseFloat(cstyle.borderLeftWidth) + parseFloat(cstyle.paddingLeft); + y += framerect.top + parseFloat(cstyle.borderTopWidth) + parseFloat(cstyle.paddingTop); + + parentFrame = win.frameElement; + } + + if (aInScreenCoords) { + x += win.mozInnerScreenX; + y += win.mozInnerScreenY; + } + + let fullZoom = win.getInterface(Ci.nsIDOMWindowUtils).fullZoom; + rect = { + left: x * fullZoom, + top: y * fullZoom, + width: rect.width * fullZoom, + height: rect.height * fullZoom + }; + + return rect; + }, + + onBeforeLinkTraversal: function(originalTarget, linkURI, linkNode, isAppTab) { + // Don't modify non-default targets or targets that aren't in top-level app + // tab docshells (isAppTab will be false for app tab subframes). + if (originalTarget != "" || !isAppTab) + return originalTarget; + + // External links from within app tabs should always open in new tabs + // instead of replacing the app tab's page (Bug 575561) + let linkHost; + let docHost; + try { + linkHost = linkURI.host; + docHost = linkNode.ownerDocument.documentURIObject.host; + } catch (e) { + // nsIURI.host can throw for non-nsStandardURL nsIURIs. + // If we fail to get either host, just return originalTarget. + return originalTarget; + } + + if (docHost == linkHost) + return originalTarget; + + // Special case: ignore "www" prefix if it is part of host string + let [longHost, shortHost] = + linkHost.length > docHost.length ? [linkHost, docHost] : [docHost, linkHost]; + if (longHost == "www." + shortHost) + return originalTarget; + + return "_blank"; + }, + + /** + * Map the plugin's name to a filtered version more suitable for UI. + * + * @param aName The full-length name string of the plugin. + * @return the simplified name string. + */ + makeNicePluginName: function (aName) { + if (aName == "Shockwave Flash") + return "Adobe Flash"; + // Regex checks if aName begins with "Java" + non-letter char + if (/^Java\W/.exec(aName)) + return "Java"; + + // Clean up the plugin name by stripping off parenthetical clauses, + // trailing version numbers or "plugin". + // EG, "Foo Bar (Linux) Plugin 1.23_02" --> "Foo Bar" + // Do this by first stripping the numbers, etc. off the end, and then + // removing "Plugin" (and then trimming to get rid of any whitespace). + // (Otherwise, something like "Java(TM) Plug-in 1.7.0_07" gets mangled) + let newName = aName.replace(/\(.*?\)/g, ""). + replace(/[\s\d\.\-\_\(\)]+$/, ""). + replace(/\bplug-?in\b/i, "").trim(); + return newName; + }, + + /** + * Return true if linkNode has a rel="noreferrer" attribute. + * + * @param linkNode The <a> element, or null. + * @return a boolean indicating if linkNode has a rel="noreferrer" attribute. + */ + linkHasNoReferrer: function (linkNode) { + // A null linkNode typically means that we're checking a link that wasn't + // provided via an <a> link, like a text-selected URL. Don't leak + // referrer information in this case. + if (!linkNode) + return true; + + let rel = linkNode.getAttribute("rel"); + if (!rel) + return false; + + // The HTML spec says that rel should be split on spaces before looking + // for particular rel values. + let values = rel.split(/[ \t\r\n\f]/); + return values.indexOf('noreferrer') != -1; + }, + + /** + * Returns true if |mimeType| is text-based, or false otherwise. + * + * @param mimeType + * The MIME type to check. + */ + mimeTypeIsTextBased: function(mimeType) { + return mimeType.startsWith("text/") || + mimeType.endsWith("+xml") || + mimeType == "application/x-javascript" || + mimeType == "application/javascript" || + mimeType == "application/json" || + mimeType == "application/xml" || + mimeType == "mozilla.application/cached-xul"; + }, + + /** + * Return true if we should FAYT for this node + window (could be CPOW): + * + * @param elt + * The element that is focused + * @param win + * The window that is focused + * + */ + shouldFastFind: function(elt, win) { + if (elt) { + if (elt instanceof win.HTMLInputElement && elt.mozIsTextField(false)) + return false; + + if (elt.isContentEditable || win.document.designMode == "on") + return false; + + if (elt instanceof win.HTMLTextAreaElement || + elt instanceof win.HTMLSelectElement || + elt instanceof win.HTMLObjectElement || + elt instanceof win.HTMLEmbedElement) + return false; + } + + return true; + }, + + /** + * Return true if we can FAYT for this window (could be CPOW): + * + * @param win + * The top level window that is focused + * + */ + canFastFind: function(win) { + if (!win) + return false; + + if (!this.mimeTypeIsTextBased(win.document.contentType)) + return false; + + // disable FAYT in about:blank to prevent FAYT opening unexpectedly. + let loc = win.location; + if (loc.href == "about:blank") + return false; + + // disable FAYT in documents that ask for it to be disabled. + if ((loc.protocol == "about:" || loc.protocol == "chrome:") && + (win.document.documentElement && + win.document.documentElement.getAttribute("disablefastfind") == "true")) + return false; + + return true; + }, + + _visibleToolbarsMap: new WeakMap(), + + /** + * Return true if any or a specific toolbar that interacts with the content + * document is visible. + * + * @param {nsIDocShell} docShell The docShell instance that a toolbar should + * be interacting with + * @param {String} which Identifier of a specific toolbar + * @return {Boolean} + */ + isToolbarVisible(docShell, which) { + let window = this.getRootWindow(docShell); + if (!this._visibleToolbarsMap.has(window)) + return false; + let toolbars = this._visibleToolbarsMap.get(window); + return !!toolbars && toolbars.has(which); + }, + + /** + * Track whether a toolbar is visible for a given a docShell. + * + * @param {nsIDocShell} docShell The docShell instance that a toolbar should + * be interacting with + * @param {String} which Identifier of a specific toolbar + * @param {Boolean} [visible] Whether the toolbar is visible. Optional, + * defaults to `true`. + */ + trackToolbarVisibility(docShell, which, visible = true) { + // We have to get the root window object, because XPConnect WrappedNatives + // can't be used as WeakMap keys. + let window = this.getRootWindow(docShell); + let toolbars = this._visibleToolbarsMap.get(window); + if (!toolbars) { + toolbars = new Set(); + this._visibleToolbarsMap.set(window, toolbars); + } + if (!visible) + toolbars.delete(which); + else + toolbars.add(which); + }, + + /** + * Retrieve the root window object (i.e. the top-most content global) for a + * specific docShell object. + * + * @param {nsIDocShell} docShell + * @return {nsIDOMWindow} + */ + getRootWindow(docShell) { + return docShell.QueryInterface(Ci.nsIDocShellTreeItem) + .sameTypeRootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + }, + + getSelectionDetails: function(topWindow, aCharLen) { + // selections of more than 150 characters aren't useful + const kMaxSelectionLen = 150; + const charLen = Math.min(aCharLen || kMaxSelectionLen, kMaxSelectionLen); + + let focusedWindow = {}; + let focusedElement = Services.focus.getFocusedElementForWindow(topWindow, true, focusedWindow); + focusedWindow = focusedWindow.value; + + let selection = focusedWindow.getSelection(); + let selectionStr = selection.toString(); + + let collapsed = selection.isCollapsed; + + let url; + let linkText; + if (selectionStr) { + // Have some text, let's figure out if it looks like a URL that isn't + // actually a link. + linkText = selectionStr.trim(); + if (/^(?:https?|ftp):/i.test(linkText)) { + try { + url = this.makeURI(linkText); + } catch (ex) {} + } + // Check if this could be a valid url, just missing the protocol. + else if (/^(?:[a-z\d-]+\.)+[a-z]+$/i.test(linkText)) { + // Now let's see if this is an intentional link selection. Our guess is + // based on whether the selection begins/ends with whitespace or is + // preceded/followed by a non-word character. + + // selection.toString() trims trailing whitespace, so we look for + // that explicitly in the first and last ranges. + let beginRange = selection.getRangeAt(0); + let delimitedAtStart = /^\s/.test(beginRange); + if (!delimitedAtStart) { + let container = beginRange.startContainer; + let offset = beginRange.startOffset; + if (container.nodeType == container.TEXT_NODE && offset > 0) + delimitedAtStart = /\W/.test(container.textContent[offset - 1]); + else + delimitedAtStart = true; + } + + let delimitedAtEnd = false; + if (delimitedAtStart) { + let endRange = selection.getRangeAt(selection.rangeCount - 1); + delimitedAtEnd = /\s$/.test(endRange); + if (!delimitedAtEnd) { + let container = endRange.endContainer; + let offset = endRange.endOffset; + if (container.nodeType == container.TEXT_NODE && + offset < container.textContent.length) + delimitedAtEnd = /\W/.test(container.textContent[offset]); + else + delimitedAtEnd = true; + } + } + + if (delimitedAtStart && delimitedAtEnd) { + let uriFixup = Cc["@mozilla.org/docshell/urifixup;1"] + .getService(Ci.nsIURIFixup); + try { + url = uriFixup.createFixupURI(linkText, uriFixup.FIXUP_FLAG_NONE); + } catch (ex) {} + } + } + } + + // try getting a selected text in text input. + if (!selectionStr && focusedElement instanceof Ci.nsIDOMNSEditableElement) { + // Don't get the selection for password fields. See bug 565717. + if (focusedElement instanceof Ci.nsIDOMHTMLTextAreaElement || + (focusedElement instanceof Ci.nsIDOMHTMLInputElement && + focusedElement.mozIsTextField(true))) { + selectionStr = focusedElement.editor.selection.toString(); + } + } + + if (selectionStr) { + if (selectionStr.length > charLen) { + // only use the first charLen important chars. see bug 221361 + var pattern = new RegExp("^(?:\\s*.){0," + charLen + "}"); + pattern.test(selectionStr); + selectionStr = RegExp.lastMatch; + } + + selectionStr = selectionStr.trim().replace(/\s+/g, " "); + + if (selectionStr.length > charLen) { + selectionStr = selectionStr.substr(0, charLen); + } + } + + if (url && !url.host) { + url = null; + } + + return { text: selectionStr, docSelectionIsCollapsed: collapsed, + linkURL: url ? url.spec : null, linkText: url ? linkText : "" }; + }, + + // Iterates through every docshell in the window and calls PermitUnload. + canCloseWindow(window) { + let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation); + let node = docShell.QueryInterface(Ci.nsIDocShellTreeItem); + for (let i = 0; i < node.childCount; ++i) { + let docShell = node.getChildAt(i).QueryInterface(Ci.nsIDocShell); + let contentViewer = docShell.contentViewer; + if (contentViewer && !contentViewer.permitUnload()) { + return false; + } + } + + return true; + }, + + /** + * Replaces %s or %S in the provided url or postData with the given parameter, + * acccording to the best charset for the given url. + * + * @return [url, postData] + * @throws if nor url nor postData accept a param, but a param was provided. + */ + parseUrlAndPostData: Task.async(function* (url, postData, param) { + let hasGETParam = /%s/i.test(url) + let decodedPostData = postData ? unescape(postData) : ""; + let hasPOSTParam = /%s/i.test(decodedPostData); + + if (!hasGETParam && !hasPOSTParam) { + if (param) { + // If nor the url, nor postData contain parameters, but a parameter was + // provided, return the original input. + throw new Error("A param was provided but there's nothing to bind it to"); + } + return [url, postData]; + } + + let charset = ""; + const re = /^(.*)\&mozcharset=([a-zA-Z][_\-a-zA-Z0-9]+)\s*$/; + let matches = url.match(re); + if (matches) { + [, url, charset] = matches; + } else { + // Try to fetch a charset from History. + try { + // Will return an empty string if character-set is not found. + charset = yield PlacesUtils.getCharsetForURI(this.makeURI(url)); + } catch (ex) { + // makeURI() throws if url is invalid. + Cu.reportError(ex); + } + } + + // encodeURIComponent produces UTF-8, and cannot be used for other charsets. + // escape() works in those cases, but it doesn't uri-encode +, @, and /. + // Therefore we need to manually replace these ASCII characters by their + // encodeURIComponent result, to match the behavior of nsEscape() with + // url_XPAlphas. + let encodedParam = ""; + if (charset && charset != "UTF-8") { + try { + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = charset; + encodedParam = converter.ConvertFromUnicode(param) + converter.Finish(); + } catch (ex) { + encodedParam = param; + } + encodedParam = escape(encodedParam).replace(/[+@\/]+/g, encodeURIComponent); + } else { + // Default charset is UTF-8 + encodedParam = encodeURIComponent(param); + } + + url = url.replace(/%s/g, encodedParam).replace(/%S/g, param); + if (hasPOSTParam) { + postData = decodedPostData.replace(/%s/g, encodedParam) + .replace(/%S/g, param); + } + return [url, postData]; + }), +}; |