mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 02:09:05 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			716 lines
		
	
	
	
		
			19 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			716 lines
		
	
	
	
		
			19 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";
 | 
						|
 | 
						|
const EXPORTED_SYMBOLS = ["FaviconLoader"];
 | 
						|
 | 
						|
const lazy = {};
 | 
						|
 | 
						|
ChromeUtils.defineESModuleGetters(lazy, {
 | 
						|
  DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
 | 
						|
  PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
 | 
						|
});
 | 
						|
 | 
						|
const STREAM_SEGMENT_SIZE = 4096;
 | 
						|
const PR_UINT32_MAX = 0xffffffff;
 | 
						|
 | 
						|
const BinaryInputStream = Components.Constructor(
 | 
						|
  "@mozilla.org/binaryinputstream;1",
 | 
						|
  "nsIBinaryInputStream",
 | 
						|
  "setInputStream"
 | 
						|
);
 | 
						|
const StorageStream = Components.Constructor(
 | 
						|
  "@mozilla.org/storagestream;1",
 | 
						|
  "nsIStorageStream",
 | 
						|
  "init"
 | 
						|
);
 | 
						|
const BufferedOutputStream = Components.Constructor(
 | 
						|
  "@mozilla.org/network/buffered-output-stream;1",
 | 
						|
  "nsIBufferedOutputStream",
 | 
						|
  "init"
 | 
						|
);
 | 
						|
 | 
						|
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 MAX_ICON_SIZE = 2048;
 | 
						|
 | 
						|
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);
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
function promiseImage(stream, type) {
 | 
						|
  return new Promise((resolve, reject) => {
 | 
						|
    let imgTools = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools);
 | 
						|
 | 
						|
    imgTools.decodeImageAsync(
 | 
						|
      stream,
 | 
						|
      type,
 | 
						|
      (image, result) => {
 | 
						|
        if (!Components.isSuccessCode(result)) {
 | 
						|
          reject();
 | 
						|
          return;
 | 
						|
        }
 | 
						|
 | 
						|
        resolve(image);
 | 
						|
      },
 | 
						|
      Services.tm.currentThread
 | 
						|
    );
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
class FaviconLoad {
 | 
						|
  constructor(iconInfo) {
 | 
						|
    this.icon = iconInfo;
 | 
						|
 | 
						|
    let securityFlags;
 | 
						|
    if (iconInfo.node.crossOrigin === "anonymous") {
 | 
						|
      securityFlags = Ci.nsILoadInfo.SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT;
 | 
						|
    } else if (iconInfo.node.crossOrigin === "use-credentials") {
 | 
						|
      securityFlags =
 | 
						|
        Ci.nsILoadInfo.SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT |
 | 
						|
        Ci.nsILoadInfo.SEC_COOKIES_INCLUDE;
 | 
						|
    } else {
 | 
						|
      securityFlags =
 | 
						|
        Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT;
 | 
						|
    }
 | 
						|
 | 
						|
    this.channel = Services.io.newChannelFromURI(
 | 
						|
      iconInfo.iconUri,
 | 
						|
      iconInfo.node,
 | 
						|
      iconInfo.node.nodePrincipal,
 | 
						|
      iconInfo.node.nodePrincipal,
 | 
						|
      securityFlags |
 | 
						|
        Ci.nsILoadInfo.SEC_ALLOW_CHROME |
 | 
						|
        Ci.nsILoadInfo.SEC_DISALLOW_SCRIPT,
 | 
						|
      Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_FAVICON
 | 
						|
    );
 | 
						|
 | 
						|
    if (this.channel instanceof Ci.nsIHttpChannel) {
 | 
						|
      this.channel.QueryInterface(Ci.nsIHttpChannel);
 | 
						|
      let referrerInfo = Cc["@mozilla.org/referrer-info;1"].createInstance(
 | 
						|
        Ci.nsIReferrerInfo
 | 
						|
      );
 | 
						|
      // Sometimes node is a document and sometimes it is an element. We need
 | 
						|
      // to set the referrer info correctly either way.
 | 
						|
      if (iconInfo.node.nodeType == iconInfo.node.DOCUMENT_NODE) {
 | 
						|
        referrerInfo.initWithDocument(iconInfo.node);
 | 
						|
      } else {
 | 
						|
        referrerInfo.initWithElement(iconInfo.node);
 | 
						|
      }
 | 
						|
      this.channel.referrerInfo = referrerInfo;
 | 
						|
    }
 | 
						|
    this.channel.loadFlags |=
 | 
						|
      Ci.nsIRequest.LOAD_BACKGROUND |
 | 
						|
      Ci.nsIRequest.VALIDATE_NEVER |
 | 
						|
      Ci.nsIRequest.LOAD_FROM_CACHE;
 | 
						|
    // 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 (this.channel instanceof Ci.nsIHttpChannelInternal) {
 | 
						|
      this.channel.blockAuthPrompt = true;
 | 
						|
    }
 | 
						|
 | 
						|
    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 = lazy.PromiseUtils.defer();
 | 
						|
 | 
						|
    // Clear the references when we succeed or fail.
 | 
						|
    let cleanup = () => {
 | 
						|
      this.channel = null;
 | 
						|
      this.dataBuffer = null;
 | 
						|
      this.stream = null;
 | 
						|
    };
 | 
						|
    this._deferred.promise.then(cleanup, cleanup);
 | 
						|
 | 
						|
    this.dataBuffer = new StorageStream(STREAM_SEGMENT_SIZE, PR_UINT32_MAX);
 | 
						|
 | 
						|
    // storage streams do not implement writeFrom so wrap it with a buffered stream.
 | 
						|
    this.stream = new BufferedOutputStream(
 | 
						|
      this.dataBuffer.getOutputStream(0),
 | 
						|
      STREAM_SEGMENT_SIZE * 2
 | 
						|
    );
 | 
						|
 | 
						|
    try {
 | 
						|
      this.channel.asyncOpen(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) {}
 | 
						|
 | 
						|
  onDataAvailable(request, inputStream, offset, count) {
 | 
						|
    this.stream.writeFrom(inputStream, count);
 | 
						|
  }
 | 
						|
 | 
						|
  asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
 | 
						|
    if (oldChannel == this.channel) {
 | 
						|
      this.channel = newChannel;
 | 
						|
    }
 | 
						|
 | 
						|
    callback.onRedirectVerifyCallback(Cr.NS_OK);
 | 
						|
  }
 | 
						|
 | 
						|
  async onStopRequest(request, statusCode) {
 | 
						|
    if (request != this.channel) {
 | 
						|
      // Indicates that a redirect has occurred. We don't care about the result
 | 
						|
      // of the original channel.
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    this.stream.close();
 | 
						|
    this.stream = null;
 | 
						|
 | 
						|
    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}.`,
 | 
						|
            { data: { httpStatus: this.channel.responseStatus } }
 | 
						|
          )
 | 
						|
        );
 | 
						|
        return;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // By default don't store icons added after "pageshow".
 | 
						|
    let canStoreIcon = this.icon.beforePageShow;
 | 
						|
    if (canStoreIcon) {
 | 
						|
      // Don't store icons responding with Cache-Control: no-store, but always
 | 
						|
      // allow root domain icons.
 | 
						|
      try {
 | 
						|
        if (
 | 
						|
          this.icon.iconUri.filePath != "/favicon.ico" &&
 | 
						|
          this.channel instanceof Ci.nsIHttpChannel &&
 | 
						|
          this.channel.isNoStoreResponse()
 | 
						|
        ) {
 | 
						|
          canStoreIcon = false;
 | 
						|
        }
 | 
						|
      } catch (ex) {
 | 
						|
        if (ex.result != Cr.NS_ERROR_NOT_AVAILABLE) {
 | 
						|
          throw ex;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // 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 stream = new BinaryInputStream(this.dataBuffer.newInputStream(0));
 | 
						|
      let buffer = new ArrayBuffer(this.dataBuffer.length);
 | 
						|
      stream.readArrayBuffer(buffer.byteLength, buffer);
 | 
						|
 | 
						|
      let type = this.channel.contentType;
 | 
						|
      let blob = new Blob([buffer], { type });
 | 
						|
 | 
						|
      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 image;
 | 
						|
        try {
 | 
						|
          image = await promiseImage(this.dataBuffer.newInputStream(0), type);
 | 
						|
        } catch (e) {
 | 
						|
          throw Components.Exception(
 | 
						|
            `Favicon at "${this.icon.iconUri.spec}" could not be decoded.`,
 | 
						|
            Cr.NS_ERROR_FAILURE
 | 
						|
          );
 | 
						|
        }
 | 
						|
 | 
						|
        if (image.width > MAX_ICON_SIZE || image.height > MAX_ICON_SIZE) {
 | 
						|
          throw Components.Exception(
 | 
						|
            `Favicon at "${this.icon.iconUri.spec}" is too large.`,
 | 
						|
            Cr.NS_ERROR_FAILURE
 | 
						|
          );
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      let dataURL = await promiseBlobAsDataURL(blob);
 | 
						|
 | 
						|
      this._deferred.resolve({
 | 
						|
        expiration,
 | 
						|
        dataURL,
 | 
						|
        canStoreIcon,
 | 
						|
      });
 | 
						|
    } catch (e) {
 | 
						|
      this._deferred.reject(e);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  getInterface(iid) {
 | 
						|
    if (iid.equals(Ci.nsIChannelEventSink)) {
 | 
						|
      return this;
 | 
						|
    }
 | 
						|
    throw Components.Exception("", 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;
 | 
						|
  }
 | 
						|
 | 
						|
  // 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;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // 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) {
 | 
						|
    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 {
 | 
						|
    richIcon: largestRichIcon,
 | 
						|
    tabIcon,
 | 
						|
  };
 | 
						|
}
 | 
						|
 | 
						|
class IconLoader {
 | 
						|
  constructor(actor) {
 | 
						|
    this.actor = actor;
 | 
						|
  }
 | 
						|
 | 
						|
  async load(iconInfo) {
 | 
						|
    if (this._loader) {
 | 
						|
      this._loader.cancel();
 | 
						|
    }
 | 
						|
 | 
						|
    if (LOCAL_FAVICON_SCHEMES.includes(iconInfo.iconUri.scheme)) {
 | 
						|
      // We need to do a manual security check because the channel won't do
 | 
						|
      // it for us.
 | 
						|
      try {
 | 
						|
        Services.scriptSecurityManager.checkLoadURIWithPrincipal(
 | 
						|
          iconInfo.node.nodePrincipal,
 | 
						|
          iconInfo.iconUri,
 | 
						|
          Services.scriptSecurityManager.ALLOW_CHROME
 | 
						|
        );
 | 
						|
      } catch (ex) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      this.actor.sendAsyncMessage("Link:SetIcon", {
 | 
						|
        pageURL: iconInfo.pageUri.spec,
 | 
						|
        originalURL: iconInfo.iconUri.spec,
 | 
						|
        canUseForTab: !iconInfo.isRichIcon,
 | 
						|
        expiration: undefined,
 | 
						|
        iconURL: iconInfo.iconUri.spec,
 | 
						|
        canStoreIcon: iconInfo.beforePageShow,
 | 
						|
      });
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Let the main process that a tab icon is possibly coming.
 | 
						|
    this.actor.sendAsyncMessage("Link:LoadingIcon", {
 | 
						|
      originalURL: iconInfo.iconUri.spec,
 | 
						|
      canUseForTab: !iconInfo.isRichIcon,
 | 
						|
    });
 | 
						|
 | 
						|
    try {
 | 
						|
      this._loader = new FaviconLoad(iconInfo);
 | 
						|
      let { dataURL, expiration, canStoreIcon } = await this._loader.load();
 | 
						|
 | 
						|
      this.actor.sendAsyncMessage("Link:SetIcon", {
 | 
						|
        pageURL: iconInfo.pageUri.spec,
 | 
						|
        originalURL: iconInfo.iconUri.spec,
 | 
						|
        canUseForTab: !iconInfo.isRichIcon,
 | 
						|
        expiration,
 | 
						|
        iconURL: dataURL,
 | 
						|
        canStoreIcon,
 | 
						|
      });
 | 
						|
    } catch (e) {
 | 
						|
      if (e.result != Cr.NS_BINDING_ABORTED) {
 | 
						|
        if (typeof e.data?.wrappedJSObject?.httpStatus !== "number") {
 | 
						|
          console.error(e);
 | 
						|
        }
 | 
						|
 | 
						|
        // Used mainly for tests currently.
 | 
						|
        this.actor.sendAsyncMessage("Link:SetFailedIcon", {
 | 
						|
          originalURL: iconInfo.iconUri.spec,
 | 
						|
          canUseForTab: !iconInfo.isRichIcon,
 | 
						|
        });
 | 
						|
      }
 | 
						|
    } finally {
 | 
						|
      this._loader = null;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  cancel() {
 | 
						|
    if (!this._loader) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    this._loader.cancel();
 | 
						|
    this._loader = null;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class FaviconLoader {
 | 
						|
  constructor(actor) {
 | 
						|
    this.actor = actor;
 | 
						|
    this.iconInfos = [];
 | 
						|
 | 
						|
    // Icons added after onPageShow() are likely added by modifying <link> tags
 | 
						|
    // through javascript; we want to avoid storing those permanently because
 | 
						|
    // they are probably used to show badges, and many of them could be
 | 
						|
    // randomly generated. This boolean can be used to track that case.
 | 
						|
    this.beforePageShow = true;
 | 
						|
 | 
						|
    // 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(actor);
 | 
						|
    this.tabIconLoader = new IconLoader(actor);
 | 
						|
 | 
						|
    this.iconTask = new lazy.DeferredTask(
 | 
						|
      () => this.loadIcons(),
 | 
						|
      FAVICON_PARSING_TIMEOUT
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  loadIcons() {
 | 
						|
    // If the page is unloaded immediately after the DeferredTask's timer fires
 | 
						|
    // we can still attempt to load icons, which will fail since the content
 | 
						|
    // window is no longer available. Checking if iconInfos has been cleared
 | 
						|
    // allows us to bail out early in this case.
 | 
						|
    if (!this.iconInfos.length) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let preferredWidth =
 | 
						|
      PREFERRED_WIDTH * Math.ceil(this.actor.contentWindow.devicePixelRatio);
 | 
						|
    let { richIcon, tabIcon } = selectIcons(this.iconInfos, preferredWidth);
 | 
						|
    this.iconInfos = [];
 | 
						|
 | 
						|
    if (richIcon) {
 | 
						|
      this.richIconLoader.load(richIcon);
 | 
						|
    }
 | 
						|
 | 
						|
    if (tabIcon) {
 | 
						|
      this.tabIconLoader.load(tabIcon);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  addIconFromLink(aLink, aIsRichIcon) {
 | 
						|
    let iconInfo = makeFaviconFromLink(aLink, aIsRichIcon);
 | 
						|
    if (iconInfo) {
 | 
						|
      iconInfo.beforePageShow = this.beforePageShow;
 | 
						|
      this.iconInfos.push(iconInfo);
 | 
						|
      this.iconTask.arm();
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  addDefaultIcon(pageUri) {
 | 
						|
    // Currently ImageDocuments will just load the default favicon, see bug
 | 
						|
    // 403651 for discussion.
 | 
						|
    this.iconInfos.push({
 | 
						|
      pageUri,
 | 
						|
      iconUri: pageUri
 | 
						|
        .mutate()
 | 
						|
        .setPathQueryRef("/favicon.ico")
 | 
						|
        .finalize(),
 | 
						|
      width: -1,
 | 
						|
      isRichIcon: false,
 | 
						|
      type: TYPE_ICO,
 | 
						|
      node: this.actor.document,
 | 
						|
      beforePageShow: this.beforePageShow,
 | 
						|
    });
 | 
						|
    this.iconTask.arm();
 | 
						|
  }
 | 
						|
 | 
						|
  onPageShow() {
 | 
						|
    // We're likely done with icon parsing so load the pending icons now.
 | 
						|
    if (this.iconTask.isArmed) {
 | 
						|
      this.iconTask.disarm();
 | 
						|
      this.loadIcons();
 | 
						|
    }
 | 
						|
    this.beforePageShow = false;
 | 
						|
  }
 | 
						|
 | 
						|
  onPageHide() {
 | 
						|
    this.richIconLoader.cancel();
 | 
						|
    this.tabIconLoader.cancel();
 | 
						|
 | 
						|
    this.iconTask.disarm();
 | 
						|
    this.iconInfos = [];
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function makeFaviconFromLink(aLink, aIsRichIcon) {
 | 
						|
  let iconUri = getLinkIconURI(aLink);
 | 
						|
  if (!iconUri) {
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
 | 
						|
  // Extract the size type and width.
 | 
						|
  let width = extractIconSize(aLink.sizes);
 | 
						|
 | 
						|
  return {
 | 
						|
    pageUri: aLink.ownerDocument.documentURIObject,
 | 
						|
    iconUri,
 | 
						|
    width,
 | 
						|
    isRichIcon: aIsRichIcon,
 | 
						|
    type: aLink.type,
 | 
						|
    node: aLink,
 | 
						|
  };
 | 
						|
}
 |