1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 ChromeUtils.defineESModuleGetters(lazy, {
8 DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
9 PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
12 const STREAM_SEGMENT_SIZE = 4096;
13 const PR_UINT32_MAX = 0xffffffff;
15 const BinaryInputStream = Components.Constructor(
16 "@mozilla.org/binaryinputstream;1",
17 "nsIBinaryInputStream",
20 const StorageStream = Components.Constructor(
21 "@mozilla.org/storagestream;1",
25 const BufferedOutputStream = Components.Constructor(
26 "@mozilla.org/network/buffered-output-stream;1",
27 "nsIBufferedOutputStream",
31 const SIZES_TELEMETRY_ENUM = {
38 const FAVICON_PARSING_TIMEOUT = 100;
39 const FAVICON_RICH_ICON_MIN_WIDTH = 96;
40 const PREFERRED_WIDTH = 16;
42 // URL schemes that we don't want to load and convert to data URLs.
43 const LOCAL_FAVICON_SCHEMES = ["chrome", "about", "resource", "data"];
45 const MAX_FAVICON_EXPIRATION = 7 * 24 * 60 * 60 * 1000;
46 const MAX_ICON_SIZE = 2048;
48 const TYPE_ICO = "image/x-icon";
49 const TYPE_SVG = "image/svg+xml";
51 function promiseBlobAsDataURL(blob) {
52 return new Promise((resolve, reject) => {
53 let reader = new FileReader();
54 reader.addEventListener("load", () => resolve(reader.result));
55 reader.addEventListener("error", reject);
56 reader.readAsDataURL(blob);
60 function promiseBlobAsOctets(blob) {
61 return new Promise((resolve, reject) => {
62 let reader = new FileReader();
63 reader.addEventListener("load", () => {
64 resolve(Array.from(reader.result).map(c => c.charCodeAt(0)));
66 reader.addEventListener("error", reject);
67 reader.readAsBinaryString(blob);
71 function promiseImage(stream, type) {
72 return new Promise((resolve, reject) => {
73 let imgTools = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools);
75 imgTools.decodeImageAsync(
79 if (!Components.isSuccessCode(result)) {
86 Services.tm.currentThread
92 constructor(iconInfo) {
96 if (iconInfo.node.crossOrigin === "anonymous") {
97 securityFlags = Ci.nsILoadInfo.SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT;
98 } else if (iconInfo.node.crossOrigin === "use-credentials") {
100 Ci.nsILoadInfo.SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT |
101 Ci.nsILoadInfo.SEC_COOKIES_INCLUDE;
104 Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT;
107 this.channel = Services.io.newChannelFromURI(
110 iconInfo.node.nodePrincipal,
111 iconInfo.node.nodePrincipal,
113 Ci.nsILoadInfo.SEC_ALLOW_CHROME |
114 Ci.nsILoadInfo.SEC_DISALLOW_SCRIPT,
115 Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_FAVICON
118 if (this.channel instanceof Ci.nsIHttpChannel) {
119 this.channel.QueryInterface(Ci.nsIHttpChannel);
120 let referrerInfo = Cc["@mozilla.org/referrer-info;1"].createInstance(
123 // Sometimes node is a document and sometimes it is an element. We need
124 // to set the referrer info correctly either way.
125 if (iconInfo.node.nodeType == iconInfo.node.DOCUMENT_NODE) {
126 referrerInfo.initWithDocument(iconInfo.node);
128 referrerInfo.initWithElement(iconInfo.node);
130 this.channel.referrerInfo = referrerInfo;
132 this.channel.loadFlags |=
133 Ci.nsIRequest.LOAD_BACKGROUND |
134 Ci.nsIRequest.VALIDATE_NEVER |
135 Ci.nsIRequest.LOAD_FROM_CACHE;
136 // Sometimes node is a document and sometimes it is an element. This is
137 // the easiest single way to get to the load group in both those cases.
138 this.channel.loadGroup =
139 iconInfo.node.ownerGlobal.document.documentLoadGroup;
140 this.channel.notificationCallbacks = this;
142 if (this.channel instanceof Ci.nsIHttpChannelInternal) {
143 this.channel.blockAuthPrompt = true;
147 Services.prefs.getBoolPref("network.http.tailing.enabled", true) &&
148 this.channel instanceof Ci.nsIClassOfService
150 this.channel.addClassFlags(
151 Ci.nsIClassOfService.Tail | Ci.nsIClassOfService.Throttleable
157 this._deferred = lazy.PromiseUtils.defer();
159 // Clear the references when we succeed or fail.
160 let cleanup = () => {
162 this.dataBuffer = null;
165 this._deferred.promise.then(cleanup, cleanup);
167 this.dataBuffer = new StorageStream(STREAM_SEGMENT_SIZE, PR_UINT32_MAX);
169 // storage streams do not implement writeFrom so wrap it with a buffered stream.
170 this.stream = new BufferedOutputStream(
171 this.dataBuffer.getOutputStream(0),
172 STREAM_SEGMENT_SIZE * 2
176 this.channel.asyncOpen(this);
178 this._deferred.reject(e);
181 return this._deferred.promise;
189 this.channel.cancel(Cr.NS_BINDING_ABORTED);
192 onStartRequest(request) {}
194 onDataAvailable(request, inputStream, offset, count) {
195 this.stream.writeFrom(inputStream, count);
198 asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
199 if (oldChannel == this.channel) {
200 this.channel = newChannel;
203 callback.onRedirectVerifyCallback(Cr.NS_OK);
206 async onStopRequest(request, statusCode) {
207 if (request != this.channel) {
208 // Indicates that a redirect has occurred. We don't care about the result
209 // of the original channel.
216 if (!Components.isSuccessCode(statusCode)) {
217 if (statusCode == Cr.NS_BINDING_ABORTED) {
218 this._deferred.reject(
219 Components.Exception(
220 `Favicon load from ${this.icon.iconUri.spec} was cancelled.`,
225 this._deferred.reject(
226 Components.Exception(
227 `Favicon at "${this.icon.iconUri.spec}" failed to load.`,
235 if (this.channel instanceof Ci.nsIHttpChannel) {
236 if (!this.channel.requestSucceeded) {
237 this._deferred.reject(
238 Components.Exception(
239 `Favicon at "${this.icon.iconUri.spec}" failed to load: ${this.channel.responseStatusText}.`,
240 { data: { httpStatus: this.channel.responseStatus } }
247 // By default don't store icons added after "pageshow".
248 let canStoreIcon = this.icon.beforePageShow;
250 // Don't store icons responding with Cache-Control: no-store, but always
251 // allow root domain icons.
254 this.icon.iconUri.filePath != "/favicon.ico" &&
255 this.channel instanceof Ci.nsIHttpChannel &&
256 this.channel.isNoStoreResponse()
258 canStoreIcon = false;
261 if (ex.result != Cr.NS_ERROR_NOT_AVAILABLE) {
267 // Attempt to get an expiration time from the cache. If this fails, we'll
269 let expiration = Date.now() + MAX_FAVICON_EXPIRATION;
271 // This stuff isn't available after onStopRequest returns (so don't start
272 // any async operations before this!).
273 if (this.channel instanceof Ci.nsICacheInfoChannel) {
275 expiration = Math.min(
276 this.channel.cacheTokenExpirationTime * 1000,
280 // Ignore failures to get the expiration time.
285 let stream = new BinaryInputStream(this.dataBuffer.newInputStream(0));
286 let buffer = new ArrayBuffer(this.dataBuffer.length);
287 stream.readArrayBuffer(buffer.byteLength, buffer);
289 let type = this.channel.contentType;
290 let blob = new Blob([buffer], { type });
292 if (type != "image/svg+xml") {
293 let octets = await promiseBlobAsOctets(blob);
294 let sniffer = Cc["@mozilla.org/image/loader;1"].createInstance(
297 type = sniffer.getMIMETypeFromContent(
304 throw Components.Exception(
305 `Favicon at "${this.icon.iconUri.spec}" did not match a known mimetype.`,
310 blob = blob.slice(0, blob.size, type);
314 image = await promiseImage(this.dataBuffer.newInputStream(0), type);
316 throw Components.Exception(
317 `Favicon at "${this.icon.iconUri.spec}" could not be decoded.`,
322 if (image.width > MAX_ICON_SIZE || image.height > MAX_ICON_SIZE) {
323 throw Components.Exception(
324 `Favicon at "${this.icon.iconUri.spec}" is too large.`,
330 let dataURL = await promiseBlobAsDataURL(blob);
332 this._deferred.resolve({
338 this._deferred.reject(e);
343 if (iid.equals(Ci.nsIChannelEventSink)) {
346 throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
351 * Extract the icon width from the size attribute. It also sends the telemetry
352 * about the size type and size dimension info.
354 * @param {Array} aSizes An array of strings about size.
355 * @return {Number} A width of the icon in pixel.
357 function extractIconSize(aSizes) {
360 const re = /^([1-9][0-9]*)x[1-9][0-9]*$/i;
363 for (let size of aSizes) {
364 if (size.toLowerCase() == "any") {
365 sizesType = SIZES_TELEMETRY_ENUM.ANY;
368 let values = re.exec(size);
369 if (values && values.length > 1) {
370 sizesType = SIZES_TELEMETRY_ENUM.DIMENSION;
371 width = parseInt(values[1]);
374 sizesType = SIZES_TELEMETRY_ENUM.INVALID;
380 sizesType = SIZES_TELEMETRY_ENUM.NO_SIZES;
383 // Telemetry probes for measuring the sizes attribute
384 // usage and available dimensions.
386 .getHistogramById("LINK_ICON_SIZES_ATTR_USAGE")
390 .getHistogramById("LINK_ICON_SIZES_ATTR_DIMENSION")
398 * Get link icon URI from a link dom node.
400 * @param {DOMNode} aLink A link dom node.
401 * @return {nsIURI} A uri of the icon.
403 function getLinkIconURI(aLink) {
404 let targetDoc = aLink.ownerDocument;
405 let uri = Services.io.newURI(aLink.href, targetDoc.characterSet);
407 uri = uri.mutate().setUserPass("").finalize();
409 // some URIs are immutable
415 * Guess a type for an icon based on its declared type or file extension.
417 function guessType(icon) {
418 // No type with no icon
423 // Use the file extension to guess at a type we're interested in
425 let extension = icon.iconUri.filePath.split(".").pop();
434 // Fuzzily prefer the type or fall back to the declared type
435 return icon.type == "image/vnd.microsoft.icon" ? TYPE_ICO : icon.type || "";
439 * Selects the best rich icon and tab icon from a list of IconInfo objects.
441 * @param {Array} iconInfos A list of IconInfo objects.
442 * @param {integer} preferredWidth The preferred width for tab icons.
444 function selectIcons(iconInfos, preferredWidth) {
445 if (!iconInfos.length) {
454 // Other links with the "icon" tag are the default icons
456 // Rich icons are either apple-touch or fluid icons, or the ones of the
457 // dimension 96x96 or greater
460 for (let icon of iconInfos) {
461 if (!icon.isRichIcon) {
462 // First check for svg. If it's not available check for an icon with a
463 // size adapt to the current resolution. If both are not available, prefer
464 // ico files. When multiple icons are in the same set, the latest wins.
465 if (guessType(icon) == TYPE_SVG) {
466 preferredIcon = icon;
468 icon.width == preferredWidth &&
469 guessType(preferredIcon) != TYPE_SVG
471 preferredIcon = icon;
473 guessType(icon) == TYPE_ICO &&
474 (!preferredIcon || guessType(preferredIcon) == TYPE_ICO)
476 preferredIcon = icon;
479 // Check for an icon larger yet closest to preferredWidth, that can be
480 // downscaled efficiently.
482 icon.width >= preferredWidth &&
483 (!bestSizedIcon || bestSizedIcon.width >= icon.width)
485 bestSizedIcon = icon;
489 // Note that some sites use hi-res icons without specifying them as
490 // apple-touch or fluid icons.
491 if (icon.isRichIcon || icon.width >= FAVICON_RICH_ICON_MIN_WIDTH) {
492 if (!largestRichIcon || largestRichIcon.width < icon.width) {
493 largestRichIcon = icon;
500 // Now set the favicons for the page in the following order:
501 // 1. Set the best rich icon if any.
502 // 2. Set the preferred one if any, otherwise check if there's a better
504 // This order allows smaller icon frames to eventually override rich icon
509 tabIcon = preferredIcon;
510 } else if (bestSizedIcon) {
511 tabIcon = bestSizedIcon;
512 } else if (defaultIcon) {
513 tabIcon = defaultIcon;
517 richIcon: largestRichIcon,
527 async load(iconInfo) {
529 this._loader.cancel();
532 if (LOCAL_FAVICON_SCHEMES.includes(iconInfo.iconUri.scheme)) {
533 // We need to do a manual security check because the channel won't do
536 Services.scriptSecurityManager.checkLoadURIWithPrincipal(
537 iconInfo.node.nodePrincipal,
539 Services.scriptSecurityManager.ALLOW_CHROME
544 this.actor.sendAsyncMessage("Link:SetIcon", {
545 pageURL: iconInfo.pageUri.spec,
546 originalURL: iconInfo.iconUri.spec,
547 canUseForTab: !iconInfo.isRichIcon,
548 expiration: undefined,
549 iconURL: iconInfo.iconUri.spec,
550 canStoreIcon: iconInfo.beforePageShow,
555 // Let the main process that a tab icon is possibly coming.
556 this.actor.sendAsyncMessage("Link:LoadingIcon", {
557 originalURL: iconInfo.iconUri.spec,
558 canUseForTab: !iconInfo.isRichIcon,
562 this._loader = new FaviconLoad(iconInfo);
563 let { dataURL, expiration, canStoreIcon } = await this._loader.load();
565 this.actor.sendAsyncMessage("Link:SetIcon", {
566 pageURL: iconInfo.pageUri.spec,
567 originalURL: iconInfo.iconUri.spec,
568 canUseForTab: !iconInfo.isRichIcon,
574 if (e.result != Cr.NS_BINDING_ABORTED) {
575 if (typeof e.data?.wrappedJSObject?.httpStatus !== "number") {
579 // Used mainly for tests currently.
580 this.actor.sendAsyncMessage("Link:SetFailedIcon", {
581 originalURL: iconInfo.iconUri.spec,
582 canUseForTab: !iconInfo.isRichIcon,
595 this._loader.cancel();
600 export class FaviconLoader {
605 // Icons added after onPageShow() are likely added by modifying <link> tags
606 // through javascript; we want to avoid storing those permanently because
607 // they are probably used to show badges, and many of them could be
608 // randomly generated. This boolean can be used to track that case.
609 this.beforePageShow = true;
611 // For every page we attempt to find a rich icon and a tab icon. These
612 // objects take care of the load process for each.
613 this.richIconLoader = new IconLoader(actor);
614 this.tabIconLoader = new IconLoader(actor);
616 this.iconTask = new lazy.DeferredTask(
617 () => this.loadIcons(),
618 FAVICON_PARSING_TIMEOUT
623 // If the page is unloaded immediately after the DeferredTask's timer fires
624 // we can still attempt to load icons, which will fail since the content
625 // window is no longer available. Checking if iconInfos has been cleared
626 // allows us to bail out early in this case.
627 if (!this.iconInfos.length) {
632 PREFERRED_WIDTH * Math.ceil(this.actor.contentWindow.devicePixelRatio);
633 let { richIcon, tabIcon } = selectIcons(this.iconInfos, preferredWidth);
637 this.richIconLoader.load(richIcon);
641 this.tabIconLoader.load(tabIcon);
645 addIconFromLink(aLink, aIsRichIcon) {
646 let iconInfo = makeFaviconFromLink(aLink, aIsRichIcon);
648 iconInfo.beforePageShow = this.beforePageShow;
649 this.iconInfos.push(iconInfo);
656 addDefaultIcon(pageUri) {
657 // Currently ImageDocuments will just load the default favicon, see bug
658 // 403651 for discussion.
659 this.iconInfos.push({
661 iconUri: pageUri.mutate().setPathQueryRef("/favicon.ico").finalize(),
665 node: this.actor.document,
666 beforePageShow: this.beforePageShow,
672 // We're likely done with icon parsing so load the pending icons now.
673 if (this.iconTask.isArmed) {
674 this.iconTask.disarm();
677 this.beforePageShow = false;
681 this.richIconLoader.cancel();
682 this.tabIconLoader.cancel();
684 this.iconTask.disarm();
689 function makeFaviconFromLink(aLink, aIsRichIcon) {
690 let iconUri = getLinkIconURI(aLink);
695 // Extract the size type and width.
696 let width = extractIconSize(aLink.sizes);
699 pageUri: aLink.ownerDocument.documentURIObject,
702 isRichIcon: aIsRichIcon,