tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

FaviconLoader.sys.mjs (20508B)


      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/. */
      4 
      5 // Bug 1924775 - ESLint doesn't yet know about `ImageDecoder`.
      6 /* globals ImageDecoder:false */
      7 
      8 import {
      9  TYPE_SVG,
     10  TYPE_ICO,
     11  TRUSTED_FAVICON_SCHEMES,
     12  blobAsDataURL,
     13 } from "moz-src:///browser/modules/FaviconUtils.sys.mjs";
     14 
     15 const lazy = {};
     16 
     17 ChromeUtils.defineESModuleGetters(lazy, {
     18  DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
     19 });
     20 
     21 const STREAM_SEGMENT_SIZE = 4096;
     22 const PR_UINT32_MAX = 0xffffffff;
     23 
     24 const BinaryInputStream = Components.Constructor(
     25  "@mozilla.org/binaryinputstream;1",
     26  "nsIBinaryInputStream",
     27  "setInputStream"
     28 );
     29 const StorageStream = Components.Constructor(
     30  "@mozilla.org/storagestream;1",
     31  "nsIStorageStream",
     32  "init"
     33 );
     34 const BufferedOutputStream = Components.Constructor(
     35  "@mozilla.org/network/buffered-output-stream;1",
     36  "nsIBufferedOutputStream",
     37  "init"
     38 );
     39 
     40 const SIZES_TELEMETRY_ENUM = {
     41  NO_SIZES: 0,
     42  ANY: 1,
     43  DIMENSION: 2,
     44  INVALID: 3,
     45 };
     46 
     47 const FAVICON_PARSING_TIMEOUT = 100;
     48 const FAVICON_RICH_ICON_MIN_WIDTH = 96;
     49 const PREFERRED_WIDTH = 16;
     50 
     51 const MAX_FAVICON_EXPIRATION = 7 * 24 * 60 * 60 * 1000;
     52 const MAX_ICON_SIZE = 2048;
     53 
     54 async function decodeImage({
     55  url,
     56  type,
     57  data,
     58  transfer,
     59  desiredWidth,
     60  desiredHeight,
     61 }) {
     62  let image;
     63  try {
     64    let decoder = new ImageDecoder({
     65      type,
     66      data,
     67      desiredWidth,
     68      desiredHeight,
     69      transfer: transfer ? [data] : undefined,
     70    });
     71 
     72    let result = await decoder.decode({ completeFramesOnly: true });
     73    image = result.image;
     74  } catch {
     75    throw Components.Exception(
     76      `Favicon at "${url}" could not be decoded.`,
     77      Cr.NS_ERROR_FAILURE
     78    );
     79  }
     80 
     81  if (
     82    image.displayWidth > MAX_ICON_SIZE ||
     83    image.displayHeight > MAX_ICON_SIZE
     84  ) {
     85    throw Components.Exception(
     86      `Favicon at "${url}" is too large.`,
     87      Cr.NS_ERROR_FAILURE
     88    );
     89  }
     90 
     91  let imageBuffer = new ArrayBuffer(image.allocationSize());
     92  await image.copyTo(imageBuffer);
     93  return {
     94    blob: new Blob([imageBuffer]),
     95    format: image.format,
     96    displayWidth: image.displayWidth,
     97    displayHeight: image.displayHeight,
     98  };
     99 }
    100 
    101 // Convert image data bytes to an array of blobs with associated format/size info.
    102 async function convertImage(url, type, data) {
    103  if (type == TYPE_ICO) {
    104    try {
    105      let decoder = new ImageDecoder({
    106        type,
    107        data,
    108      });
    109      await decoder.tracks.ready;
    110      let sizes = decoder.tracks[0].getSizes();
    111      if (sizes.length > 1) {
    112        return Promise.all(
    113          sizes.map(({ width, height }) =>
    114            decodeImage({
    115              url,
    116              type,
    117              // Can't transfer the data buffer, because we decode multiple times.
    118              transfer: false,
    119              data,
    120              // Decode the ICO image at the different sizes of contained images.
    121              desiredWidth: width,
    122              desiredHeight: height,
    123            })
    124          )
    125        );
    126      }
    127    } catch {}
    128  }
    129 
    130  let image = await decodeImage({
    131    url,
    132    type,
    133    transfer: true,
    134    data,
    135  });
    136  return [image];
    137 }
    138 
    139 class FaviconLoad {
    140  constructor(iconInfo) {
    141    this.icon = iconInfo;
    142 
    143    let securityFlags;
    144    if (iconInfo.node.crossOrigin === "anonymous") {
    145      securityFlags = Ci.nsILoadInfo.SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT;
    146    } else if (iconInfo.node.crossOrigin === "use-credentials") {
    147      securityFlags =
    148        Ci.nsILoadInfo.SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT |
    149        Ci.nsILoadInfo.SEC_COOKIES_INCLUDE;
    150    } else {
    151      securityFlags =
    152        Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT;
    153    }
    154 
    155    this.channel = Services.io.newChannelFromURI(
    156      iconInfo.iconUri,
    157      iconInfo.node,
    158      iconInfo.node.nodePrincipal,
    159      iconInfo.node.nodePrincipal,
    160      securityFlags |
    161        Ci.nsILoadInfo.SEC_ALLOW_CHROME |
    162        Ci.nsILoadInfo.SEC_DISALLOW_SCRIPT,
    163      Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_FAVICON
    164    );
    165 
    166    if (this.channel instanceof Ci.nsIHttpChannel) {
    167      this.channel.QueryInterface(Ci.nsIHttpChannel);
    168      let referrerInfo = Cc["@mozilla.org/referrer-info;1"].createInstance(
    169        Ci.nsIReferrerInfo
    170      );
    171      // Sometimes node is a document and sometimes it is an element. We need
    172      // to set the referrer info correctly either way.
    173      if (iconInfo.node.nodeType == iconInfo.node.DOCUMENT_NODE) {
    174        referrerInfo.initWithDocument(iconInfo.node);
    175      } else {
    176        referrerInfo.initWithElement(iconInfo.node);
    177      }
    178      this.channel.referrerInfo = referrerInfo;
    179    }
    180    this.channel.loadFlags |=
    181      Ci.nsIRequest.LOAD_BACKGROUND |
    182      Ci.nsIRequest.VALIDATE_NEVER |
    183      Ci.nsIRequest.LOAD_FROM_CACHE;
    184    // Sometimes node is a document and sometimes it is an element. This is
    185    // the easiest single way to get to the load group in both those cases.
    186    this.channel.loadGroup =
    187      iconInfo.node.ownerGlobal.document.documentLoadGroup;
    188    this.channel.notificationCallbacks = this;
    189 
    190    if (this.channel instanceof Ci.nsIHttpChannelInternal) {
    191      this.channel.blockAuthPrompt = true;
    192    }
    193 
    194    if (
    195      Services.prefs.getBoolPref("network.http.tailing.enabled", true) &&
    196      this.channel instanceof Ci.nsIClassOfService
    197    ) {
    198      this.channel.addClassFlags(
    199        Ci.nsIClassOfService.Tail | Ci.nsIClassOfService.Throttleable
    200      );
    201    }
    202  }
    203 
    204  load() {
    205    this._deferred = Promise.withResolvers();
    206 
    207    // Clear the references when we succeed or fail.
    208    let cleanup = () => {
    209      this.channel = null;
    210      this.dataBuffer = null;
    211      this.stream = null;
    212    };
    213    this._deferred.promise.then(cleanup, cleanup);
    214 
    215    this.dataBuffer = new StorageStream(STREAM_SEGMENT_SIZE, PR_UINT32_MAX);
    216 
    217    // storage streams do not implement writeFrom so wrap it with a buffered stream.
    218    this.stream = new BufferedOutputStream(
    219      this.dataBuffer.getOutputStream(0),
    220      STREAM_SEGMENT_SIZE * 2
    221    );
    222 
    223    try {
    224      this.channel.asyncOpen(this);
    225    } catch (e) {
    226      this._deferred.reject(e);
    227    }
    228 
    229    return this._deferred.promise;
    230  }
    231 
    232  cancel() {
    233    if (!this.channel) {
    234      return;
    235    }
    236 
    237    this.channel.cancel(Cr.NS_BINDING_ABORTED);
    238  }
    239 
    240  onStartRequest() {}
    241 
    242  onDataAvailable(request, inputStream, offset, count) {
    243    this.stream.writeFrom(inputStream, count);
    244  }
    245 
    246  asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
    247    if (oldChannel == this.channel) {
    248      this.channel = newChannel;
    249    }
    250 
    251    callback.onRedirectVerifyCallback(Cr.NS_OK);
    252  }
    253 
    254  async onStopRequest(request, statusCode) {
    255    if (request != this.channel) {
    256      // Indicates that a redirect has occurred. We don't care about the result
    257      // of the original channel.
    258      return;
    259    }
    260 
    261    this.stream.close();
    262    this.stream = null;
    263 
    264    if (!Components.isSuccessCode(statusCode)) {
    265      if (statusCode == Cr.NS_BINDING_ABORTED) {
    266        this._deferred.reject(
    267          Components.Exception(
    268            `Favicon load from ${this.icon.iconUri.spec} was cancelled.`,
    269            statusCode
    270          )
    271        );
    272      } else {
    273        this._deferred.reject(
    274          Components.Exception(
    275            `Favicon at "${this.icon.iconUri.spec}" failed to load.`,
    276            statusCode
    277          )
    278        );
    279      }
    280      return;
    281    }
    282 
    283    if (this.channel instanceof Ci.nsIHttpChannel) {
    284      if (!this.channel.requestSucceeded) {
    285        this._deferred.reject(
    286          Components.Exception(
    287            `Favicon at "${this.icon.iconUri.spec}" failed to load: ${this.channel.responseStatusText}.`,
    288            { data: { httpStatus: this.channel.responseStatus } }
    289          )
    290        );
    291        return;
    292      }
    293    }
    294 
    295    // By default we don't store icons added after the `pageshow` event as they
    296    // may be used to show a badge, indicate a service status, or other form
    297    // of icon animations.
    298    let canStoreIcon = this.icon.beforePageShow;
    299    // We make an exception for root icons, as they are unlikely to be used
    300    // as status indicators, and in general they are always usable.
    301    if (this.icon.iconUri.filePath == "/favicon.ico") {
    302      canStoreIcon = true;
    303    } else {
    304      // Do not store non-root icons if `Cache-Control: no-store` header is set.
    305      try {
    306        if (
    307          this.channel instanceof Ci.nsIHttpChannel &&
    308          this.channel.isNoStoreResponse()
    309        ) {
    310          canStoreIcon = false;
    311        }
    312      } catch (ex) {
    313        if (ex.result != Cr.NS_ERROR_NOT_AVAILABLE) {
    314          throw ex;
    315        }
    316      }
    317    }
    318 
    319    // Attempt to get an expiration time from the cache.  If this fails, we'll
    320    // use this default.
    321    let expiration = Date.now() + MAX_FAVICON_EXPIRATION;
    322 
    323    // This stuff isn't available after onStopRequest returns (so don't start
    324    // any async operations before this!).
    325    if (this.channel instanceof Ci.nsICacheInfoChannel) {
    326      try {
    327        expiration = Math.min(
    328          this.channel.cacheTokenExpirationTime * 1000,
    329          expiration
    330        );
    331      } catch (e) {
    332        // Ignore failures to get the expiration time.
    333      }
    334    }
    335 
    336    try {
    337      let stream = new BinaryInputStream(this.dataBuffer.newInputStream(0));
    338      let buffer = new ArrayBuffer(this.dataBuffer.length);
    339      stream.readArrayBuffer(buffer.byteLength, buffer);
    340 
    341      let type = this.channel.contentType;
    342      let images, dataURL;
    343      if (type != "image/svg+xml") {
    344        let octets = new Uint8Array(buffer);
    345        let sniffer = Cc["@mozilla.org/image/loader;1"].createInstance(
    346          Ci.nsIContentSniffer
    347        );
    348        type = sniffer.getMIMETypeFromContent(
    349          this.channel,
    350          octets,
    351          octets.length
    352        );
    353 
    354        if (!type) {
    355          throw Components.Exception(
    356            `Favicon at "${this.icon.iconUri.spec}" did not match a known mimetype.`,
    357            Cr.NS_ERROR_FAILURE
    358          );
    359        }
    360 
    361        images = await convertImage(this.icon.iconUri.spec, type, buffer);
    362      } else {
    363        dataURL = await blobAsDataURL(new Blob([buffer], { type }));
    364      }
    365 
    366      this._deferred.resolve({
    367        expiration,
    368        images,
    369        dataURL,
    370        canStoreIcon,
    371      });
    372    } catch (e) {
    373      this._deferred.reject(e);
    374    }
    375  }
    376 
    377  getInterface(iid) {
    378    if (iid.equals(Ci.nsIChannelEventSink)) {
    379      return this;
    380    }
    381    throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
    382  }
    383 }
    384 
    385 /**
    386 * Extract the icon width from the size attribute. It also sends the telemetry
    387 * about the size type and size dimension info.
    388 *
    389 * @param {Array} aSizes An array of strings about size.
    390 * @return {number} A width of the icon in pixel.
    391 */
    392 function extractIconSize(aSizes) {
    393  let width = -1;
    394  let sizesType;
    395  const re = /^([1-9][0-9]*)x[1-9][0-9]*$/i;
    396 
    397  if (aSizes.length) {
    398    for (let size of aSizes) {
    399      if (size.toLowerCase() == "any") {
    400        sizesType = SIZES_TELEMETRY_ENUM.ANY;
    401        break;
    402      } else {
    403        let values = re.exec(size);
    404        if (values && values.length > 1) {
    405          sizesType = SIZES_TELEMETRY_ENUM.DIMENSION;
    406          width = parseInt(values[1]);
    407          break;
    408        } else {
    409          sizesType = SIZES_TELEMETRY_ENUM.INVALID;
    410          break;
    411        }
    412      }
    413    }
    414  } else {
    415    sizesType = SIZES_TELEMETRY_ENUM.NO_SIZES;
    416  }
    417 
    418  // Telemetry probes for measuring the sizes attribute
    419  // usage and available dimensions.
    420  Glean.linkIconSizesAttr.usage.accumulateSingleSample(sizesType);
    421  if (width > 0) {
    422    Glean.linkIconSizesAttr.dimension.accumulateSingleSample(width);
    423  }
    424 
    425  return width;
    426 }
    427 
    428 /**
    429 * Get link icon URI from a link dom node.
    430 *
    431 * @param {DOMNode} aLink A link dom node.
    432 * @return {nsIURI} A uri of the icon.
    433 */
    434 function getLinkIconURI(aLink) {
    435  let targetDoc = aLink.ownerDocument;
    436  let uri = Services.io.newURI(aLink.href, targetDoc.characterSet);
    437  try {
    438    uri = uri.mutate().setUserPass("").finalize();
    439  } catch (e) {
    440    // some URIs are immutable
    441  }
    442  return uri;
    443 }
    444 
    445 /**
    446 * Guess a type for an icon based on its declared type or file extension.
    447 */
    448 function guessType(icon) {
    449  // No type with no icon
    450  if (!icon) {
    451    return "";
    452  }
    453 
    454  // Use the file extension to guess at a type we're interested in
    455  if (!icon.type) {
    456    let extension = icon.iconUri.filePath.split(".").pop();
    457    switch (extension) {
    458      case "ico":
    459        return TYPE_ICO;
    460      case "svg":
    461        return TYPE_SVG;
    462    }
    463  }
    464 
    465  // Fuzzily prefer the type or fall back to the declared type
    466  return icon.type == "image/vnd.microsoft.icon" ? TYPE_ICO : icon.type || "";
    467 }
    468 
    469 /**
    470 * Selects the best rich icon and tab icon from a list of IconInfo objects.
    471 *
    472 * @param {Array} iconInfos A list of IconInfo objects.
    473 * @param {integer} preferredWidth The preferred width for tab icons.
    474 */
    475 function selectIcons(iconInfos, preferredWidth) {
    476  if (!iconInfos.length) {
    477    return {
    478      richIcon: null,
    479      tabIcon: null,
    480    };
    481  }
    482 
    483  let preferredIcon;
    484  let bestSizedIcon;
    485  // Other links with the "icon" tag are the default icons
    486  let defaultIcon;
    487  // Rich icons are either apple-touch or fluid icons, or the ones of the
    488  // dimension 96x96 or greater
    489  let largestRichIcon;
    490 
    491  for (let icon of iconInfos) {
    492    if (!icon.isRichIcon) {
    493      // First check for svg. If it's not available check for an icon with a
    494      // size adapt to the current resolution. If both are not available, prefer
    495      // ico files. When multiple icons are in the same set, the latest wins.
    496      if (guessType(icon) == TYPE_SVG) {
    497        preferredIcon = icon;
    498      } else if (
    499        icon.width == preferredWidth &&
    500        guessType(preferredIcon) != TYPE_SVG
    501      ) {
    502        preferredIcon = icon;
    503      } else if (
    504        guessType(icon) == TYPE_ICO &&
    505        (!preferredIcon || guessType(preferredIcon) == TYPE_ICO)
    506      ) {
    507        preferredIcon = icon;
    508      }
    509 
    510      // Check for an icon larger yet closest to preferredWidth, that can be
    511      // downscaled efficiently.
    512      if (
    513        icon.width >= preferredWidth &&
    514        (!bestSizedIcon || bestSizedIcon.width >= icon.width)
    515      ) {
    516        bestSizedIcon = icon;
    517      }
    518    }
    519 
    520    // Note that some sites use hi-res icons without specifying them as
    521    // apple-touch or fluid icons.
    522    if (icon.isRichIcon || icon.width >= FAVICON_RICH_ICON_MIN_WIDTH) {
    523      if (!largestRichIcon || largestRichIcon.width < icon.width) {
    524        largestRichIcon = icon;
    525      }
    526    } else {
    527      defaultIcon = icon;
    528    }
    529  }
    530 
    531  // Now set the favicons for the page in the following order:
    532  // 1. Set the best rich icon if any.
    533  // 2. Set the preferred one if any, otherwise check if there's a better
    534  //    sized fit.
    535  // This order allows smaller icon frames to eventually override rich icon
    536  // frames.
    537 
    538  let tabIcon = null;
    539  if (preferredIcon) {
    540    tabIcon = preferredIcon;
    541  } else if (bestSizedIcon) {
    542    tabIcon = bestSizedIcon;
    543  } else if (defaultIcon) {
    544    tabIcon = defaultIcon;
    545  }
    546 
    547  return {
    548    richIcon: largestRichIcon,
    549    tabIcon,
    550  };
    551 }
    552 
    553 class IconLoader {
    554  constructor(actor) {
    555    this.actor = actor;
    556  }
    557 
    558  async load(iconInfo) {
    559    if (this._loader) {
    560      // If we're already loading this icon, just let it finish.
    561      if (this._loader.icon.iconUri.equals(iconInfo.iconUri)) {
    562        return;
    563      }
    564      this._loader.cancel();
    565    }
    566 
    567    if (TRUSTED_FAVICON_SCHEMES.includes(iconInfo.iconUri.scheme)) {
    568      // We need to do a manual security check because the channel won't do
    569      // it for us.
    570      try {
    571        Services.scriptSecurityManager.checkLoadURIWithPrincipal(
    572          iconInfo.node.nodePrincipal,
    573          iconInfo.iconUri,
    574          Services.scriptSecurityManager.ALLOW_CHROME
    575        );
    576      } catch (ex) {
    577        return;
    578      }
    579      this.actor.sendAsyncMessage("Link:SetIcon", {
    580        pageURL: iconInfo.pageUri.spec,
    581        originalURL: iconInfo.iconUri.spec,
    582        expiration: undefined,
    583        iconURL: iconInfo.iconUri.spec,
    584        canStoreIcon:
    585          iconInfo.beforePageShow && iconInfo.iconUri.schemeIs("data"),
    586        beforePageShow: iconInfo.beforePageShow,
    587        isRichIcon: iconInfo.isRichIcon,
    588      });
    589      return;
    590    }
    591 
    592    // Let the main process that a tab icon is possibly coming.
    593    this.actor.sendAsyncMessage("Link:LoadingIcon", {
    594      originalURL: iconInfo.iconUri.spec,
    595      isRichIcon: iconInfo.isRichIcon,
    596    });
    597 
    598    try {
    599      this._loader = new FaviconLoad(iconInfo);
    600      let { dataURL, images, expiration, canStoreIcon } =
    601        await this._loader.load();
    602 
    603      this.actor.sendAsyncMessage("Link:SetIcon", {
    604        pageURL: iconInfo.pageUri.spec,
    605        originalURL: iconInfo.iconUri.spec,
    606        expiration,
    607        iconURL: dataURL,
    608        images,
    609        canStoreIcon,
    610        beforePageShow: iconInfo.beforePageShow,
    611        isRichIcon: iconInfo.isRichIcon,
    612      });
    613    } catch (e) {
    614      if (e.result != Cr.NS_BINDING_ABORTED) {
    615        if (typeof e.data?.wrappedJSObject?.httpStatus !== "number") {
    616          console.error(e);
    617        }
    618 
    619        // Used mainly for tests currently.
    620        this.actor.sendAsyncMessage("Link:SetFailedIcon", {
    621          originalURL: iconInfo.iconUri.spec,
    622          isRichIcon: iconInfo.isRichIcon,
    623        });
    624      }
    625    } finally {
    626      this._loader = null;
    627    }
    628  }
    629 
    630  cancel() {
    631    if (!this._loader) {
    632      return;
    633    }
    634 
    635    this._loader.cancel();
    636    this._loader = null;
    637  }
    638 }
    639 
    640 export class FaviconLoader {
    641  constructor(actor) {
    642    this.actor = actor;
    643    this.iconInfos = [];
    644 
    645    // Icons added after onPageShow() are likely added by modifying <link> tags
    646    // through javascript; we want to avoid storing those permanently because
    647    // they are probably used to show badges, and many of them could be
    648    // randomly generated. This boolean can be used to track that case.
    649    this.beforePageShow = true;
    650 
    651    // For every page we attempt to find a rich icon and a tab icon. These
    652    // objects take care of the load process for each.
    653    this.richIconLoader = new IconLoader(actor);
    654    this.tabIconLoader = new IconLoader(actor);
    655 
    656    this.iconTask = new lazy.DeferredTask(
    657      () => this.loadIcons(),
    658      FAVICON_PARSING_TIMEOUT
    659    );
    660  }
    661 
    662  loadIcons() {
    663    // If the page is unloaded immediately after the DeferredTask's timer fires
    664    // we can still attempt to load icons, which will fail since the content
    665    // window is no longer available. Checking if iconInfos has been cleared
    666    // allows us to bail out early in this case.
    667    if (!this.iconInfos.length) {
    668      return;
    669    }
    670 
    671    let preferredWidth =
    672      PREFERRED_WIDTH * Math.ceil(this.actor.contentWindow.devicePixelRatio);
    673    let { richIcon, tabIcon } = selectIcons(this.iconInfos, preferredWidth);
    674    this.iconInfos = [];
    675 
    676    if (richIcon) {
    677      this.richIconLoader.load(richIcon).catch(console.error);
    678    }
    679 
    680    if (tabIcon) {
    681      this.tabIconLoader.load(tabIcon).catch(console.error);
    682    }
    683  }
    684 
    685  addIconFromLink(aLink, aIsRichIcon) {
    686    let iconInfo = makeFaviconFromLink(aLink, aIsRichIcon);
    687    if (iconInfo) {
    688      iconInfo.beforePageShow = this.beforePageShow;
    689      this.iconInfos.push(iconInfo);
    690      this.iconTask.arm();
    691      return true;
    692    }
    693    return false;
    694  }
    695 
    696  addDefaultIcon(pageUri) {
    697    // Currently ImageDocuments will just load the default favicon, see bug
    698    // 403651 for discussion.
    699    this.iconInfos.push({
    700      pageUri,
    701      iconUri: pageUri.mutate().setPathQueryRef("/favicon.ico").finalize(),
    702      width: -1,
    703      isRichIcon: false,
    704      type: TYPE_ICO,
    705      node: this.actor.document,
    706      beforePageShow: this.beforePageShow,
    707    });
    708    this.iconTask.arm();
    709  }
    710 
    711  onPageShow() {
    712    // We're likely done with icon parsing so load the pending icons now.
    713    if (this.iconTask.isArmed) {
    714      this.iconTask.disarm();
    715      this.loadIcons();
    716    }
    717    this.beforePageShow = false;
    718  }
    719 
    720  onPageHide() {
    721    this.richIconLoader.cancel();
    722    this.tabIconLoader.cancel();
    723 
    724    this.iconTask.disarm();
    725    this.iconInfos = [];
    726  }
    727 }
    728 
    729 function makeFaviconFromLink(aLink, aIsRichIcon) {
    730  let iconUri = getLinkIconURI(aLink);
    731  if (!iconUri) {
    732    return null;
    733  }
    734 
    735  // Extract the size type and width.
    736  let width = extractIconSize(aLink.sizes);
    737 
    738  return {
    739    pageUri: aLink.ownerDocument.documentURIObject,
    740    iconUri,
    741    width,
    742    isRichIcon: aIsRichIcon,
    743    type: aLink.type,
    744    node: aLink,
    745  };
    746 }