rename from browser/modules/ContentLinkHandler.jsm
rename to browser/actors/LinkHandlerChild.jsm
--- a/browser/modules/ContentLinkHandler.jsm
+++ b/browser/actors/LinkHandlerChild.jsm
@@ -1,573 +1,93 @@
/* 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";
-const EXPORTED_SYMBOLS = ["ContentLinkHandler"];
+const EXPORTED_SYMBOLS = ["LinkHandlerChild"];
-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
-
-XPCOMUtils.defineLazyGlobalGetters(this, ["Blob", "FileReader"]);
+ChromeUtils.import("resource://gre/modules/ActorChild.jsm");
ChromeUtils.defineModuleGetter(this, "Feeds",
"resource:///modules/Feeds.jsm");
-ChromeUtils.defineModuleGetter(this, "DeferredTask",
- "resource://gre/modules/DeferredTask.jsm");
-ChromeUtils.defineModuleGetter(this, "PromiseUtils",
- "resource://gre/modules/PromiseUtils.jsm");
-
-const BinaryInputStream = Components.Constructor("@mozilla.org/binaryinputstream;1",
- "nsIBinaryInputStream", "setInputStream");
-
-const SIZES_TELEMETRY_ENUM = {
- NO_SIZES: 0,
- ANY: 1,
- DIMENSION: 2,
- INVALID: 3,
-};
-
-const FAVICON_PARSING_TIMEOUT = 100;
-const FAVICON_RICH_ICON_MIN_WIDTH = 96;
-const PREFERRED_WIDTH = 16;
-
-// URL schemes that we don't want to load and convert to data URLs.
-const LOCAL_FAVICON_SCHEMES = [
- "chrome",
- "about",
- "resource",
- "data",
-];
-
-const MAX_FAVICON_EXPIRATION = 7 * 24 * 60 * 60 * 1000;
-
-const TYPE_ICO = "image/x-icon";
-const TYPE_SVG = "image/svg+xml";
-
-function promiseBlobAsDataURL(blob) {
- return new Promise((resolve, reject) => {
- let reader = new FileReader();
- reader.addEventListener("load", () => resolve(reader.result));
- reader.addEventListener("error", reject);
- reader.readAsDataURL(blob);
- });
-}
-
-function promiseBlobAsOctets(blob) {
- return new Promise((resolve, reject) => {
- let reader = new FileReader();
- reader.addEventListener("load", () => {
- resolve(Array.from(reader.result).map(c => c.charCodeAt(0)));
- });
- reader.addEventListener("error", reject);
- reader.readAsBinaryString(blob);
- });
-}
-
-class FaviconLoad {
- constructor(iconInfo) {
- this.buffers = [];
- this.icon = iconInfo;
-
- this.channel = Services.io.newChannelFromURI2(
- iconInfo.iconUri,
- iconInfo.node,
- iconInfo.node.nodePrincipal,
- iconInfo.node.nodePrincipal,
- (Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS |
- Ci.nsILoadInfo.SEC_ALLOW_CHROME |
- Ci.nsILoadInfo.SEC_DISALLOW_SCRIPT),
- Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_FAVICON);
-
- this.channel.loadFlags |= Ci.nsIRequest.LOAD_BACKGROUND;
- // Sometimes node is a document and sometimes it is an element. This is
- // the easiest single way to get to the load group in both those cases.
- this.channel.loadGroup = iconInfo.node.ownerGlobal.document.documentLoadGroup;
- this.channel.notificationCallbacks = this;
-
- if (Services.prefs.getBoolPref("network.http.tailing.enabled", true) &&
- this.channel instanceof Ci.nsIClassOfService) {
- this.channel.addClassFlags(Ci.nsIClassOfService.Tail | Ci.nsIClassOfService.Throttleable);
- }
- }
-
- load() {
- this._deferred = PromiseUtils.defer();
- // Clear the channel reference when we succeed or fail.
- this._deferred.promise.then(
- () => this.channel = null,
- () => this.channel = null
- );
-
- try {
- this.channel.asyncOpen2(this);
- } catch (e) {
- this._deferred.reject(e);
- }
-
- return this._deferred.promise;
- }
-
- cancel() {
- if (!this.channel) {
- return;
- }
-
- this.channel.cancel(Cr.NS_BINDING_ABORTED);
- }
-
- onStartRequest(request, context) {
- }
+ChromeUtils.defineModuleGetter(this, "FaviconLoader",
+ "resource:///modules/FaviconLoader.jsm");
- onDataAvailable(request, context, inputStream, offset, count) {
- let stream = new BinaryInputStream(inputStream);
- let buffer = new ArrayBuffer(count);
- stream.readArrayBuffer(buffer.byteLength, buffer);
- this.buffers.push(new Uint8Array(buffer));
- }
-
- asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
- if (oldChannel == this.channel) {
- this.channel = newChannel;
- }
-
- callback.onRedirectVerifyCallback(Cr.NS_OK);
- }
-
- async onStopRequest(request, context, statusCode) {
- if (request != this.channel) {
- // Indicates that a redirect has occurred. We don't care about the result
- // of the original channel.
- return;
- }
-
- if (!Components.isSuccessCode(statusCode)) {
- if (statusCode == Cr.NS_BINDING_ABORTED) {
- this._deferred.reject(Components.Exception(`Favicon load from ${this.icon.iconUri.spec} was cancelled.`, statusCode));
- } else {
- this._deferred.reject(Components.Exception(`Favicon at "${this.icon.iconUri.spec}" failed to load.`, statusCode));
- }
- return;
- }
-
- if (this.channel instanceof Ci.nsIHttpChannel) {
- if (!this.channel.requestSucceeded) {
- this._deferred.reject(Components.Exception(`Favicon at "${this.icon.iconUri.spec}" failed to load: ${this.channel.responseStatusText}.`, Cr.NS_ERROR_FAILURE));
- return;
- }
- }
-
- // Attempt to get an expiration time from the cache. If this fails, we'll
- // use this default.
- let expiration = Date.now() + MAX_FAVICON_EXPIRATION;
-
- // This stuff isn't available after onStopRequest returns (so don't start
- // any async operations before this!).
- if (this.channel instanceof Ci.nsICacheInfoChannel) {
- try {
- expiration = Math.min(this.channel.cacheTokenExpirationTime * 1000, expiration);
- } catch (e) {
- // Ignore failures to get the expiration time.
- }
- }
-
- try {
- let type = this.channel.contentType;
- let blob = new Blob(this.buffers, { type });
+class LinkHandlerChild extends ActorChild {
+ constructor(mm) {
+ super(mm);
- if (type != "image/svg+xml") {
- let octets = await promiseBlobAsOctets(blob);
- let sniffer = Cc["@mozilla.org/image/loader;1"].
- createInstance(Ci.nsIContentSniffer);
- type = sniffer.getMIMETypeFromContent(this.channel, octets, octets.length);
-
- if (!type) {
- throw Components.Exception(`Favicon at "${this.icon.iconUri.spec}" did not match a known mimetype.`, Cr.NS_ERROR_FAILURE);
- }
-
- blob = blob.slice(0, blob.size, type);
- }
-
- let dataURL = await promiseBlobAsDataURL(blob);
-
- this._deferred.resolve({
- expiration,
- dataURL,
- });
- } catch (e) {
- this._deferred.reject(e);
- }
- }
-
- getInterface(iid) {
- if (iid.equals(Ci.nsIChannelEventSink)) {
- return this;
- }
- throw Cr.NS_ERROR_NO_INTERFACE;
- }
-}
-
-/*
- * Extract the icon width from the size attribute. It also sends the telemetry
- * about the size type and size dimension info.
- *
- * @param {Array} aSizes An array of strings about size.
- * @return {Number} A width of the icon in pixel.
- */
-function extractIconSize(aSizes) {
- let width = -1;
- let sizesType;
- const re = /^([1-9][0-9]*)x[1-9][0-9]*$/i;
-
- if (aSizes.length) {
- for (let size of aSizes) {
- if (size.toLowerCase() == "any") {
- sizesType = SIZES_TELEMETRY_ENUM.ANY;
- break;
- } else {
- let values = re.exec(size);
- if (values && values.length > 1) {
- sizesType = SIZES_TELEMETRY_ENUM.DIMENSION;
- width = parseInt(values[1]);
- break;
- } else {
- sizesType = SIZES_TELEMETRY_ENUM.INVALID;
- break;
- }
- }
- }
- } else {
- sizesType = SIZES_TELEMETRY_ENUM.NO_SIZES;
+ this.seenTabIcon = false;
+ this._iconLoader = null;
}
- // Telemetry probes for measuring the sizes attribute
- // usage and available dimensions.
- Services.telemetry.getHistogramById("LINK_ICON_SIZES_ATTR_USAGE").add(sizesType);
- if (width > 0)
- Services.telemetry.getHistogramById("LINK_ICON_SIZES_ATTR_DIMENSION").add(width);
-
- return width;
-}
-
-/*
- * Get link icon URI from a link dom node.
- *
- * @param {DOMNode} aLink A link dom node.
- * @return {nsIURI} A uri of the icon.
- */
-function getLinkIconURI(aLink) {
- let targetDoc = aLink.ownerDocument;
- let uri = Services.io.newURI(aLink.href, targetDoc.characterSet);
- try {
- uri = uri.mutate().setUserPass("").finalize();
- } catch (e) {
- // some URIs are immutable
- }
- return uri;
-}
-
-/**
- * Guess a type for an icon based on its declared type or file extension.
- */
-function guessType(icon) {
- // No type with no icon
- if (!icon) {
- return "";
- }
-
- // Use the file extension to guess at a type we're interested in
- if (!icon.type) {
- let extension = icon.iconUri.filePath.split(".").pop();
- switch (extension) {
- case "ico":
- return TYPE_ICO;
- case "svg":
- return TYPE_SVG;
+ get iconLoader() {
+ if (!this._iconLoader) {
+ this._iconLoader = new FaviconLoader(this.mm);
}
- }
-
- // Fuzzily prefer the type or fall back to the declared type
- return icon.type == "image/vnd.microsoft.icon" ? TYPE_ICO : icon.type || "";
-}
-
-/*
- * Selects the best rich icon and tab icon from a list of IconInfo objects.
- *
- * @param {Array} iconInfos A list of IconInfo objects.
- * @param {integer} preferredWidth The preferred width for tab icons.
- */
-function selectIcons(iconInfos, preferredWidth) {
- if (iconInfos.length == 0) {
- return {
- richIcon: null,
- tabIcon: null,
- };
- }
-
- let preferredIcon;
- let bestSizedIcon;
- // Other links with the "icon" tag are the default icons
- let defaultIcon;
- // Rich icons are either apple-touch or fluid icons, or the ones of the
- // dimension 96x96 or greater
- let largestRichIcon;
-
- for (let icon of iconInfos) {
- if (!icon.isRichIcon) {
- // First check for svg. If it's not available check for an icon with a
- // size adapt to the current resolution. If both are not available, prefer
- // ico files. When multiple icons are in the same set, the latest wins.
- if (guessType(icon) == TYPE_SVG) {
- preferredIcon = icon;
- } else if (icon.width == preferredWidth && guessType(preferredIcon) != TYPE_SVG) {
- preferredIcon = icon;
- } else if (guessType(icon) == TYPE_ICO && (!preferredIcon || guessType(preferredIcon) == TYPE_ICO)) {
- preferredIcon = icon;
- }
-
- // Check for an icon larger yet closest to preferredWidth, that can be
- // downscaled efficiently.
- if (icon.width >= preferredWidth &&
- (!bestSizedIcon || bestSizedIcon.width >= icon.width)) {
- bestSizedIcon = icon;
- }
- }
-
- // Note that some sites use hi-res icons without specifying them as
- // apple-touch or fluid icons.
- if (icon.isRichIcon || icon.width >= FAVICON_RICH_ICON_MIN_WIDTH) {
- if (!largestRichIcon || largestRichIcon.width < icon.width) {
- largestRichIcon = icon;
- }
- } else {
- defaultIcon = icon;
- }
- }
-
- // Now set the favicons for the page in the following order:
- // 1. Set the best rich icon if any.
- // 2. Set the preferred one if any, otherwise check if there's a better
- // sized fit.
- // This order allows smaller icon frames to eventually override rich icon
- // frames.
-
- let tabIcon = null;
- if (preferredIcon) {
- tabIcon = preferredIcon;
- } else if (bestSizedIcon) {
- tabIcon = bestSizedIcon;
- } else if (defaultIcon) {
- tabIcon = defaultIcon;
+ return this._iconLoader;
}
- return {
- richIcon: largestRichIcon,
- tabIcon
- };
-}
-
-function makeFaviconFromLink(aLink, aIsRichIcon) {
- let iconUri = getLinkIconURI(aLink);
- if (!iconUri)
- return null;
-
- // Extract the size type and width.
- let width = extractIconSize(aLink.sizes);
-
- return {
- iconUri,
- width,
- isRichIcon: aIsRichIcon,
- type: aLink.type,
- node: aLink,
- };
-}
-
-class IconLoader {
- constructor(chromeGlobal) {
- this.chromeGlobal = chromeGlobal;
- }
-
- async load(iconInfo) {
- if (this._loader) {
- this._loader.cancel();
- }
-
- if (LOCAL_FAVICON_SCHEMES.includes(iconInfo.iconUri.scheme)) {
- this.chromeGlobal.sendAsyncMessage("Link:SetIcon", {
- originalURL: iconInfo.iconUri.spec,
- canUseForTab: !iconInfo.isRichIcon,
- expiration: undefined,
- iconURL: iconInfo.iconUri.spec,
- });
- return;
- }
-
- try {
- this._loader = new FaviconLoad(iconInfo);
- let { dataURL, expiration } = await this._loader.load();
-
- this.chromeGlobal.sendAsyncMessage("Link:SetIcon", {
- originalURL: iconInfo.iconUri.spec,
- canUseForTab: !iconInfo.isRichIcon,
- expiration,
- iconURL: dataURL,
- });
- } catch (e) {
- if (e.resultCode != Cr.NS_BINDING_ABORTED) {
- Cu.reportError(e);
-
- // Used mainly for tests currently.
- this.chromeGlobal.sendAsyncMessage("Link:SetFailedIcon", {
- originalURL: iconInfo.iconUri.spec,
- canUseForTab: !iconInfo.isRichIcon,
- });
+ addRootIcon() {
+ if (!this.seenTabIcon && Services.prefs.getBoolPref("browser.chrome.guess_favicon", true) &&
+ Services.prefs.getBoolPref("browser.chrome.site_icons", true)) {
+ // Inject the default icon. Use documentURIObject so that we do the right
+ // thing with about:-style error pages. See bug 453442
+ let baseURI = this.content.document.documentURIObject;
+ if (["http", "https"].includes(baseURI.scheme)) {
+ this.iconLoader.addDefaultIcon(baseURI);
}
- } finally {
- this._loader = null;
- }
- }
-
- cancel() {
- if (!this._loader) {
- return;
- }
-
- this._loader.cancel();
- this._loader = null;
- }
-}
-
-class ContentLinkHandler {
- constructor(chromeGlobal) {
- this.chromeGlobal = chromeGlobal;
- this.iconInfos = [];
- this.seenTabIcon = false;
-
- chromeGlobal.addEventListener("DOMLinkAdded", this);
- chromeGlobal.addEventListener("DOMLinkChanged", this);
- chromeGlobal.addEventListener("pageshow", this);
- chromeGlobal.addEventListener("pagehide", this);
- chromeGlobal.addEventListener("DOMHeadElementParsed", this);
-
- // For every page we attempt to find a rich icon and a tab icon. These
- // objects take care of the load process for each.
- this.richIconLoader = new IconLoader(chromeGlobal);
- this.tabIconLoader = new IconLoader(chromeGlobal);
-
- this.iconTask = new DeferredTask(() => this.loadIcons(), FAVICON_PARSING_TIMEOUT);
- }
-
- loadIcons() {
- let preferredWidth = PREFERRED_WIDTH * Math.ceil(this.chromeGlobal.content.devicePixelRatio);
- let { richIcon, tabIcon } = selectIcons(this.iconInfos, preferredWidth);
- this.iconInfos = [];
-
- if (richIcon) {
- this.richIconLoader.load(richIcon);
- }
-
- if (tabIcon) {
- this.tabIconLoader.load(tabIcon);
- }
- }
-
- addIcon(iconInfo) {
- if (!Services.prefs.getBoolPref("browser.chrome.site_icons", true)) {
- return;
- }
-
- if (!iconInfo.isRichIcon) {
- this.seenTabIcon = true;
- }
- this.iconInfos.push(iconInfo);
- this.iconTask.arm();
- }
-
- addRootIcon(document) {
- // If we've already seen a tab icon or if root favicons are disabled then
- // bail out.
- if (this.seenTabIcon || !Services.prefs.getBoolPref("browser.chrome.guess_favicon", true)) {
- return;
- }
-
- // Currently ImageDocuments will just load the default favicon, see bug
- // 403651 for discussion.
-
- // Inject the default icon. Use documentURIObject so that we do the right
- // thing with about:-style error pages. See bug 453442
- let baseURI = document.documentURIObject;
- if (baseURI.schemeIs("http") || baseURI.schemeIs("https")) {
- let iconUri = baseURI.mutate().setPathQueryRef("/favicon.ico").finalize();
- this.addIcon({
- iconUri,
- width: -1,
- isRichIcon: false,
- type: TYPE_ICO,
- node: document,
- });
}
}
onHeadParsed(event) {
- let document = this.chromeGlobal.content.document;
- if (event.target.ownerDocument != document) {
+ if (event.target.ownerDocument != this.content.document) {
return;
}
// Per spec icons are meant to be in the <head> tag so we should have seen
// all the icons now so add the root icon if no other tab icons have been
// seen.
- this.addRootIcon(document);
+ this.addRootIcon();
// We're likely done with icon parsing so load the pending icons now.
- if (this.iconTask.isArmed) {
- this.iconTask.disarm();
- this.loadIcons();
+ if (this._iconLoader) {
+ this._iconLoader.onPageShow();
}
}
onPageShow(event) {
- let document = this.chromeGlobal.content.document;
- if (event.target != document) {
+ if (event.target != this.content.document) {
return;
}
- // Add the root icon if it hasn't already been added. We encounter this case
- // for documents that do not have a <head> tag.
- this.addRootIcon(document);
+ this.addRootIcon();
- // If we've seen any additional icons since the start of the body element
- // load them now.
- if (this.iconTask.isArmed) {
- this.iconTask.disarm();
- this.loadIcons();
+ if (this._iconLoader) {
+ this._iconLoader.onPageShow();
}
}
onPageHide(event) {
- if (event.target != this.chromeGlobal.content.document) {
+ if (event.target != this.content.document) {
return;
}
- this.richIconLoader.cancel();
- this.tabIconLoader.cancel();
-
- this.iconTask.disarm();
- this.iconInfos = [];
- this.seenTabIcon = false;
+ if (this._iconLoader) {
+ this._iconLoader.onPageHide();
+ }
}
onLinkEvent(event) {
let link = event.target;
// Ignore sub-frames (bugs 305472, 479408).
- if (link.ownerGlobal != this.chromeGlobal.content) {
+ if (link.ownerGlobal != this.content) {
return;
}
let rel = link.rel && link.rel.toLowerCase();
if (!rel || !link.href)
return;
// Note: following booleans only work for the current link, not for the
@@ -575,83 +95,87 @@ class ContentLinkHandler {
let feedAdded = false;
let iconAdded = false;
let searchAdded = false;
let rels = {};
for (let relString of rel.split(/\s+/))
rels[relString] = true;
for (let relVal in rels) {
- let isRichIcon = true;
+ let isRichIcon = false;
switch (relVal) {
case "feed":
case "alternate":
if (!feedAdded && event.type == "DOMLinkAdded") {
if (!rels.feed && rels.alternate && rels.stylesheet)
break;
if (Feeds.isValidFeed(link, link.ownerDocument.nodePrincipal, "feed" in rels)) {
- this.chromeGlobal.sendAsyncMessage("Link:AddFeed", {
+ this.mm.sendAsyncMessage("Link:AddFeed", {
type: link.type,
href: link.href,
title: link.title,
});
feedAdded = true;
}
}
break;
- case "icon":
- isRichIcon = false;
- // Fall through to rich icon handling
case "apple-touch-icon":
case "apple-touch-icon-precomposed":
case "fluid-icon":
+ isRichIcon = true;
+ case "icon":
if (iconAdded || link.hasAttribute("mask")) { // Masked icons are not supported yet.
break;
}
- let iconInfo = makeFaviconFromLink(link, isRichIcon);
+ if (!Services.prefs.getBoolPref("browser.chrome.site_icons", true)) {
+ return;
+ }
+
+ let iconInfo = FaviconLoader.makeFaviconFromLink(link, isRichIcon);
if (iconInfo) {
- iconAdded = this.addIcon(iconInfo);
+ iconAdded = true;
+ if (!isRichIcon) {
+ this.seenTabIcon = true;
+ }
+ this.iconLoader.addIcon(iconInfo);
}
break;
case "search":
if (Services.policies && !Services.policies.isAllowed("installSearchEngine")) {
break;
}
if (!searchAdded && event.type == "DOMLinkAdded") {
let type = link.type && link.type.toLowerCase();
type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
let re = /^(?:https?|ftp):/i;
if (type == "application/opensearchdescription+xml" && link.title &&
re.test(link.href)) {
let engine = { title: link.title, href: link.href };
- this.chromeGlobal.sendAsyncMessage("Link:AddSearch", {
+ this.mm.sendAsyncMessage("Link:AddSearch", {
engine,
url: link.ownerDocument.documentURI,
});
searchAdded = true;
}
}
break;
}
}
}
handleEvent(event) {
switch (event.type) {
case "pageshow":
- this.onPageShow(event);
- break;
+ return this.onPageShow(event);
case "pagehide":
- this.onPageHide(event);
- break;
+ return this.onPageHide(event);
case "DOMHeadElementParsed":
- this.onHeadParsed(event);
- break;
+ return this.onHeadParsed(event);
default:
- this.onLinkEvent(event);
+ return this.onLinkEvent(event);
}
}
}
--- a/browser/actors/moz.build
+++ b/browser/actors/moz.build
@@ -26,16 +26,17 @@ FINAL_TARGET_FILES.actors += [
'AboutReaderChild.jsm',
'BlockedSiteChild.jsm',
'BrowserTabChild.jsm',
'ClickHandlerChild.jsm',
'ContentSearchChild.jsm',
'ContextMenuChild.jsm',
'DOMFullscreenChild.jsm',
'LightWeightThemeInstallChild.jsm',
+ 'LinkHandlerChild.jsm',
'NetErrorChild.jsm',
'OfflineAppsChild.jsm',
'PageInfoChild.jsm',
'PageMetadataChild.jsm',
'PageStyleChild.jsm',
'PluginChild.jsm',
'URIFixupChild.jsm',
'WebRTCChild.jsm',
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -8,31 +8,29 @@
/* eslint-env mozilla/frame-script */
/* eslint no-unused-vars: ["error", {args: "none"}] */
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
- ContentLinkHandler: "resource:///modules/ContentLinkHandler.jsm",
ContentMetaHandler: "resource:///modules/ContentMetaHandler.jsm",
FormSubmitObserver: "resource:///modules/FormSubmitObserver.jsm",
});
XPCOMUtils.defineLazyProxy(this, "formSubmitObserver", () => {
return new FormSubmitObserver(content, this);
}, {
// stub QI
QueryInterface: ChromeUtils.generateQI([Ci.nsIFormSubmitObserver, Ci.nsISupportsWeakReference])
});
Services.obs.addObserver(formSubmitObserver, "invalidformsubmit", true);
-new ContentLinkHandler(this);
ContentMetaHandler.init(this);
// This is a temporary hack to prevent regressions (bug 1471327).
void content;
addEventListener("DOMWindowFocus", function(event) {
sendAsyncMessage("DOMWindowFocus", {});
}, false);
--- a/browser/base/content/test/performance/browser_startup_content.js
+++ b/browser/base/content/test/performance/browser_startup_content.js
@@ -45,18 +45,18 @@ const whitelist = {
// Forms and passwords
"resource://formautofill/FormAutofill.jsm",
"resource://formautofill/FormAutofillContent.jsm",
// Browser front-end
"resource:///actors/AboutReaderChild.jsm",
"resource:///actors/BrowserTabChild.jsm",
- "resource:///modules/ContentLinkHandler.jsm",
"resource:///modules/ContentMetaHandler.jsm",
+ "resource:///modules/LinkHandlerChild.jsm",
"resource:///actors/PageStyleChild.jsm",
"resource://gre/modules/ActorChild.jsm",
"resource://gre/modules/ActorManagerChild.jsm",
"resource://gre/modules/E10SUtils.jsm",
"resource://gre/modules/PrivateBrowsingUtils.jsm",
"resource://gre/modules/ReaderMode.jsm",
"resource://gre/modules/WebProgressChild.jsm",
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -126,16 +126,29 @@ let ACTORS = {
events: {
"InstallBrowserTheme": {wantUntrusted: true},
"PreviewBrowserTheme": {wantUntrusted: true},
"ResetBrowserThemePreview": {wantUntrusted: true},
},
},
},
+ LinkHandler: {
+ child: {
+ module: "resource:///actors/LinkHandlerChild.jsm",
+ events: {
+ "DOMHeadElementParsed": {},
+ "DOMLinkAdded": {},
+ "DOMLinkChanged": {},
+ "pageshow": {},
+ "pagehide": {},
+ },
+ },
+ },
+
NetError: {
child: {
module: "resource:///actors/NetErrorChild.jsm",
events: {
"AboutNetErrorLoad": {wantUntrusted: true},
"AboutNetErrorOpenCaptivePortal": {wantUntrusted: true},
"AboutNetErrorSetAutomatic": {wantUntrusted: true},
"AboutNetErrorResetPreferences": {wantUntrusted: true},
copy from browser/modules/ContentLinkHandler.jsm
copy to browser/modules/FaviconLoader.jsm
--- a/browser/modules/ContentLinkHandler.jsm
+++ b/browser/modules/FaviconLoader.jsm
@@ -1,23 +1,21 @@
/* 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";
-const EXPORTED_SYMBOLS = ["ContentLinkHandler"];
+const EXPORTED_SYMBOLS = ["FaviconLoader"];
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyGlobalGetters(this, ["Blob", "FileReader"]);
-ChromeUtils.defineModuleGetter(this, "Feeds",
- "resource:///modules/Feeds.jsm");
ChromeUtils.defineModuleGetter(this, "DeferredTask",
"resource://gre/modules/DeferredTask.jsm");
ChromeUtils.defineModuleGetter(this, "PromiseUtils",
"resource://gre/modules/PromiseUtils.jsm");
const BinaryInputStream = Components.Constructor("@mozilla.org/binaryinputstream;1",
"nsIBinaryInputStream", "setInputStream");
@@ -362,69 +360,52 @@ function selectIcons(iconInfos, preferre
}
return {
richIcon: largestRichIcon,
tabIcon
};
}
-function makeFaviconFromLink(aLink, aIsRichIcon) {
- let iconUri = getLinkIconURI(aLink);
- if (!iconUri)
- return null;
-
- // Extract the size type and width.
- let width = extractIconSize(aLink.sizes);
-
- return {
- iconUri,
- width,
- isRichIcon: aIsRichIcon,
- type: aLink.type,
- node: aLink,
- };
-}
-
class IconLoader {
- constructor(chromeGlobal) {
- this.chromeGlobal = chromeGlobal;
+ constructor(mm) {
+ this.mm = mm;
}
async load(iconInfo) {
if (this._loader) {
this._loader.cancel();
}
if (LOCAL_FAVICON_SCHEMES.includes(iconInfo.iconUri.scheme)) {
- this.chromeGlobal.sendAsyncMessage("Link:SetIcon", {
+ this.mm.sendAsyncMessage("Link:SetIcon", {
originalURL: iconInfo.iconUri.spec,
canUseForTab: !iconInfo.isRichIcon,
expiration: undefined,
iconURL: iconInfo.iconUri.spec,
});
return;
}
try {
this._loader = new FaviconLoad(iconInfo);
let { dataURL, expiration } = await this._loader.load();
- this.chromeGlobal.sendAsyncMessage("Link:SetIcon", {
+ this.mm.sendAsyncMessage("Link:SetIcon", {
originalURL: iconInfo.iconUri.spec,
canUseForTab: !iconInfo.isRichIcon,
expiration,
iconURL: dataURL,
});
} catch (e) {
- if (e.resultCode != Cr.NS_BINDING_ABORTED) {
+ if (e.result != Cr.NS_BINDING_ABORTED) {
Cu.reportError(e);
// Used mainly for tests currently.
- this.chromeGlobal.sendAsyncMessage("Link:SetFailedIcon", {
+ this.mm.sendAsyncMessage("Link:SetFailedIcon", {
originalURL: iconInfo.iconUri.spec,
canUseForTab: !iconInfo.isRichIcon,
});
}
} finally {
this._loader = null;
}
}
@@ -434,224 +415,85 @@ class IconLoader {
return;
}
this._loader.cancel();
this._loader = null;
}
}
-class ContentLinkHandler {
- constructor(chromeGlobal) {
- this.chromeGlobal = chromeGlobal;
+class FaviconLoader {
+ constructor(mm) {
+ this.mm = mm;
this.iconInfos = [];
- this.seenTabIcon = false;
-
- chromeGlobal.addEventListener("DOMLinkAdded", this);
- chromeGlobal.addEventListener("DOMLinkChanged", this);
- chromeGlobal.addEventListener("pageshow", this);
- chromeGlobal.addEventListener("pagehide", this);
- chromeGlobal.addEventListener("DOMHeadElementParsed", this);
// For every page we attempt to find a rich icon and a tab icon. These
// objects take care of the load process for each.
- this.richIconLoader = new IconLoader(chromeGlobal);
- this.tabIconLoader = new IconLoader(chromeGlobal);
+ this.richIconLoader = new IconLoader(mm);
+ this.tabIconLoader = new IconLoader(mm);
this.iconTask = new DeferredTask(() => this.loadIcons(), FAVICON_PARSING_TIMEOUT);
}
loadIcons() {
- let preferredWidth = PREFERRED_WIDTH * Math.ceil(this.chromeGlobal.content.devicePixelRatio);
+ let preferredWidth = PREFERRED_WIDTH * Math.ceil(this.mm.content.devicePixelRatio);
let { richIcon, tabIcon } = selectIcons(this.iconInfos, preferredWidth);
this.iconInfos = [];
if (richIcon) {
this.richIconLoader.load(richIcon);
}
if (tabIcon) {
this.tabIconLoader.load(tabIcon);
}
}
addIcon(iconInfo) {
- if (!Services.prefs.getBoolPref("browser.chrome.site_icons", true)) {
- return;
- }
-
- if (!iconInfo.isRichIcon) {
- this.seenTabIcon = true;
- }
this.iconInfos.push(iconInfo);
this.iconTask.arm();
}
- addRootIcon(document) {
- // If we've already seen a tab icon or if root favicons are disabled then
- // bail out.
- if (this.seenTabIcon || !Services.prefs.getBoolPref("browser.chrome.guess_favicon", true)) {
- return;
- }
-
+ addDefaultIcon(baseURI) {
// Currently ImageDocuments will just load the default favicon, see bug
// 403651 for discussion.
-
- // Inject the default icon. Use documentURIObject so that we do the right
- // thing with about:-style error pages. See bug 453442
- let baseURI = document.documentURIObject;
- if (baseURI.schemeIs("http") || baseURI.schemeIs("https")) {
- let iconUri = baseURI.mutate().setPathQueryRef("/favicon.ico").finalize();
- this.addIcon({
- iconUri,
- width: -1,
- isRichIcon: false,
- type: TYPE_ICO,
- node: document,
- });
- }
+ this.addIcon({
+ iconUri: baseURI.mutate().setPathQueryRef("/favicon.ico").finalize(),
+ width: -1,
+ isRichIcon: false,
+ type: TYPE_ICO,
+ node: this.mm.content.document,
+ });
}
- onHeadParsed(event) {
- let document = this.chromeGlobal.content.document;
- if (event.target.ownerDocument != document) {
- return;
- }
-
- // Per spec icons are meant to be in the <head> tag so we should have seen
- // all the icons now so add the root icon if no other tab icons have been
- // seen.
- this.addRootIcon(document);
-
+ onPageShow() {
// We're likely done with icon parsing so load the pending icons now.
if (this.iconTask.isArmed) {
this.iconTask.disarm();
this.loadIcons();
}
}
- onPageShow(event) {
- let document = this.chromeGlobal.content.document;
- if (event.target != document) {
- return;
- }
-
- // Add the root icon if it hasn't already been added. We encounter this case
- // for documents that do not have a <head> tag.
- this.addRootIcon(document);
-
- // If we've seen any additional icons since the start of the body element
- // load them now.
- if (this.iconTask.isArmed) {
- this.iconTask.disarm();
- this.loadIcons();
- }
- }
-
- onPageHide(event) {
- if (event.target != this.chromeGlobal.content.document) {
- return;
- }
-
+ onPageHide() {
this.richIconLoader.cancel();
this.tabIconLoader.cancel();
this.iconTask.disarm();
this.iconInfos = [];
- this.seenTabIcon = false;
}
- onLinkEvent(event) {
- let link = event.target;
- // Ignore sub-frames (bugs 305472, 479408).
- if (link.ownerGlobal != this.chromeGlobal.content) {
- return;
- }
-
- let rel = link.rel && link.rel.toLowerCase();
- if (!rel || !link.href)
- return;
+ static makeFaviconFromLink(aLink, aIsRichIcon) {
+ let iconUri = getLinkIconURI(aLink);
+ if (!iconUri)
+ return null;
- // Note: following booleans only work for the current link, not for the
- // whole content
- let feedAdded = false;
- let iconAdded = false;
- let searchAdded = false;
- let rels = {};
- for (let relString of rel.split(/\s+/))
- rels[relString] = true;
-
- for (let relVal in rels) {
- let isRichIcon = true;
-
- switch (relVal) {
- case "feed":
- case "alternate":
- if (!feedAdded && event.type == "DOMLinkAdded") {
- if (!rels.feed && rels.alternate && rels.stylesheet)
- break;
+ // Extract the size type and width.
+ let width = extractIconSize(aLink.sizes);
- if (Feeds.isValidFeed(link, link.ownerDocument.nodePrincipal, "feed" in rels)) {
- this.chromeGlobal.sendAsyncMessage("Link:AddFeed", {
- type: link.type,
- href: link.href,
- title: link.title,
- });
- feedAdded = true;
- }
- }
- break;
- case "icon":
- isRichIcon = false;
- // Fall through to rich icon handling
- case "apple-touch-icon":
- case "apple-touch-icon-precomposed":
- case "fluid-icon":
- if (iconAdded || link.hasAttribute("mask")) { // Masked icons are not supported yet.
- break;
- }
-
- let iconInfo = makeFaviconFromLink(link, isRichIcon);
- if (iconInfo) {
- iconAdded = this.addIcon(iconInfo);
- }
- break;
- case "search":
- if (Services.policies && !Services.policies.isAllowed("installSearchEngine")) {
- break;
- }
-
- if (!searchAdded && event.type == "DOMLinkAdded") {
- let type = link.type && link.type.toLowerCase();
- type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
-
- let re = /^(?:https?|ftp):/i;
- if (type == "application/opensearchdescription+xml" && link.title &&
- re.test(link.href)) {
- let engine = { title: link.title, href: link.href };
- this.chromeGlobal.sendAsyncMessage("Link:AddSearch", {
- engine,
- url: link.ownerDocument.documentURI,
- });
- searchAdded = true;
- }
- }
- break;
- }
- }
- }
-
- handleEvent(event) {
- switch (event.type) {
- case "pageshow":
- this.onPageShow(event);
- break;
- case "pagehide":
- this.onPageHide(event);
- break;
- case "DOMHeadElementParsed":
- this.onHeadParsed(event);
- break;
- default:
- this.onLinkEvent(event);
- }
+ return {
+ iconUri,
+ width,
+ isRichIcon: aIsRichIcon,
+ type: aLink.type,
+ node: aLink,
+ };
}
}
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -119,21 +119,21 @@ XPCSHELL_TESTS_MANIFESTS += ['test/unit/
EXTRA_JS_MODULES += [
'AboutNewTab.jsm',
'AsyncTabSwitcher.jsm',
'BrowserErrorReporter.jsm',
'BrowserUsageTelemetry.jsm',
'BrowserWindowTracker.jsm',
'ContentClick.jsm',
'ContentCrashHandlers.jsm',
- 'ContentLinkHandler.jsm',
'ContentMetaHandler.jsm',
'ContentObservers.js',
'ContentSearch.jsm',
'ExtensionsUI.jsm',
+ 'FaviconLoader.jsm',
'Feeds.jsm',
'FormSubmitObserver.jsm',
'FormValidationHandler.jsm',
'HomePage.jsm',
'LaterRun.jsm',
'LightweightThemeChildHelper.jsm',
'OpenInTabsUtils.jsm',
'PageActions.jsm',