gecko-dev/browser/modules/ContentLinkHandler.jsm

358 строки
11 KiB
JavaScript

/* 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 EXPORTED_SYMBOLS = [ "ContentLinkHandler" ];
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.defineModuleGetter(this, "Feeds",
"resource:///modules/Feeds.jsm");
ChromeUtils.defineModuleGetter(this, "BrowserUtils",
"resource://gre/modules/BrowserUtils.jsm");
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 TYPE_ICO = "image/x-icon";
const TYPE_SVG = "image/svg+xml";
/*
* Create a nsITimer.
*
* @param {function} aCallback A timeout callback function.
* @param {Number} aDelay A timeout interval in millisecond.
* @return {nsITimer} A nsITimer object.
*/
function setTimeout(aCallback, aDelay) {
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
timer.initWithCallback(aCallback, aDelay, Ci.nsITimer.TYPE_ONE_SHOT);
return timer;
}
/*
* 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;
}
// 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;
}
/*
* Set the icon via sending the "Link:Seticon" message.
*
* @param {Object} aIconInfo The IconInfo object looks like {
* iconUri: icon URI,
* loadingPrincipal: icon loading principal
* }.
* @param {Object} aChromeGlobal A global chrome object.
*/
function setIconForLink(aIconInfo, aChromeGlobal) {
aChromeGlobal.sendAsyncMessage(
"Link:SetIcon",
{ url: aIconInfo.iconUri.spec,
loadingPrincipal: aIconInfo.loadingPrincipal,
requestContextID: aIconInfo.requestContextID,
canUseForTab: !aIconInfo.isRichIcon,
});
}
/**
* 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;
}
}
// Fuzzily prefer the type or fall back to the declared type
return icon.type == "image/vnd.microsoft.icon" ? TYPE_ICO : icon.type || "";
}
/*
* Timeout callback function for loading favicon.
*
* @param {Map} aFaviconLoads A map of page URL and FaviconLoad object pairs,
* where the FaviconLoad object looks like {
* timer: a nsITimer object,
* iconInfos: an array of IconInfo objects
* }
* @param {String} aPageUrl A page URL string for this callback.
* @param {Object} aChromeGlobal A global chrome object.
*/
function faviconTimeoutCallback(aFaviconLoads, aPageUrl, aChromeGlobal) {
let load = aFaviconLoads.get(aPageUrl);
if (!load)
return;
let preferredIcon;
let preferredWidth = 16 * Math.ceil(aChromeGlobal.content.devicePixelRatio);
// 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 load.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;
}
}
// 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 use the default one.
// This order allows smaller icon frames to eventually override rich icon
// frames.
if (largestRichIcon) {
setIconForLink(largestRichIcon, aChromeGlobal);
}
if (preferredIcon) {
setIconForLink(preferredIcon, aChromeGlobal);
} else if (defaultIcon) {
setIconForLink(defaultIcon, aChromeGlobal);
}
load.timer = null;
aFaviconLoads.delete(aPageUrl);
}
/*
* Get request context ID of the link dom node's document.
*
* @param {DOMNode} aLink A link dom node.
* @return {Number} The request context ID.
* Return null when document's load group is not available.
*/
function getLinkRequestContextID(aLink) {
try {
return aLink.ownerDocument.documentLoadGroup.requestContextID;
} catch (e) {
return null;
}
}
/*
* Favicon link handler.
*
* @param {DOMNode} aLink A link dom node.
* @param {bool} aIsRichIcon A bool to indicate if the link is rich icon.
* @param {Object} aChromeGlobal A global chrome object.
* @param {Map} aFaviconLoads A map of page URL and FaviconLoad object pairs.
* @return {bool} Returns true if the link is successfully handled.
*/
function handleFaviconLink(aLink, aIsRichIcon, aChromeGlobal, aFaviconLoads) {
let pageUrl = aLink.ownerDocument.documentURI;
let iconUri = getLinkIconURI(aLink);
if (!iconUri)
return false;
// Extract the size type and width.
let width = extractIconSize(aLink.sizes);
let iconInfo = {
iconUri,
width,
isRichIcon: aIsRichIcon,
type: aLink.type,
loadingPrincipal: aLink.ownerDocument.nodePrincipal,
requestContextID: getLinkRequestContextID(aLink)
};
if (aFaviconLoads.has(pageUrl)) {
let load = aFaviconLoads.get(pageUrl);
load.iconInfos.push(iconInfo);
// Re-initialize the timer
load.timer.delay = FAVICON_PARSING_TIMEOUT;
} else {
let timer = setTimeout(() => faviconTimeoutCallback(aFaviconLoads, pageUrl, aChromeGlobal),
FAVICON_PARSING_TIMEOUT);
let load = { timer, iconInfos: [iconInfo] };
aFaviconLoads.set(pageUrl, load);
}
return true;
}
var ContentLinkHandler = {
init(chromeGlobal) {
const faviconLoads = new Map();
chromeGlobal.addEventListener("DOMLinkAdded", event => {
this.onLinkEvent(event, chromeGlobal, faviconLoads);
});
chromeGlobal.addEventListener("DOMLinkChanged", event => {
this.onLinkEvent(event, chromeGlobal, faviconLoads);
});
chromeGlobal.addEventListener("unload", event => {
for (const [pageUrl, load] of faviconLoads) {
load.timer.cancel();
load.timer = null;
faviconLoads.delete(pageUrl);
}
});
},
onLinkEvent(event, chromeGlobal, faviconLoads) {
var link = event.originalTarget;
var rel = link.rel && link.rel.toLowerCase();
if (!link || !link.ownerDocument || !rel || !link.href)
return;
// Ignore sub-frames (bugs 305472, 479408).
let window = link.ownerGlobal;
if (window != window.top)
return;
// Note: following booleans only work for the current link, not for the
// whole content
var feedAdded = false;
var iconAdded = false;
var searchAdded = false;
var 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;
if (Feeds.isValidFeed(link, link.ownerDocument.nodePrincipal, "feed" in rels)) {
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 (link.hasAttribute("mask") || // Masked icons are not supported yet.
iconAdded ||
!Services.prefs.getBoolPref("browser.chrome.site_icons")) {
break;
}
iconAdded = handleFaviconLink(link, isRichIcon, chromeGlobal, faviconLoads);
break;
case "search":
if (!searchAdded && event.type == "DOMLinkAdded") {
var 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 };
chromeGlobal.sendAsyncMessage("Link:AddSearch",
{engine,
url: link.ownerDocument.documentURI});
searchAdded = true;
}
}
break;
}
}
},
};