tor-browser

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

LinkHandlerParent.sys.mjs (7050B)


      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 import {
      6  TYPE_ICO,
      7  SVG_DATA_URI_PREFIX,
      8  TRUSTED_FAVICON_SCHEMES,
      9  blobAsDataURL,
     10 } from "moz-src:///browser/modules/FaviconUtils.sys.mjs";
     11 
     12 const lazy = {};
     13 
     14 ChromeUtils.defineESModuleGetters(lazy, {
     15  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     16  OpenSearchManager:
     17    "moz-src:///browser/components/search/OpenSearchManager.sys.mjs",
     18 });
     19 
     20 let gTestListeners = new Set();
     21 
     22 async function drawImageOnCanvas(canvas, image) {
     23  let data = await image.blob.bytes();
     24  let frame = new VideoFrame(data, {
     25    timestamp: 0,
     26    format: image.format,
     27    codedWidth: image.displayWidth,
     28    codedHeight: image.displayHeight,
     29  });
     30 
     31  canvas.width = frame.displayWidth;
     32  canvas.height = frame.displayHeight;
     33  let ctx = canvas.getContext("2d");
     34  ctx.drawImage(frame, 0, 0);
     35 }
     36 
     37 // Re-construct the ICO file with different sized PNG images.
     38 // See https://en.wikipedia.org/wiki/ICO_(file_format).
     39 function createICO(images) {
     40  const ICO_HEADER_SIZE = 6;
     41  const ICO_DIR_ENTRY_SIZE = 16;
     42 
     43  const metadataSize = ICO_HEADER_SIZE + ICO_DIR_ENTRY_SIZE * images.length;
     44  const size =
     45    metadataSize + images.reduce((acc, image) => acc + image.byteLength, 0);
     46 
     47  let buffer = new ArrayBuffer(size);
     48  let u8 = new Uint8Array(buffer);
     49  let view = new DataView(buffer);
     50 
     51  view.setUint16(0, 0, true); // idReserved
     52  view.setUint16(2, 1, true); // idType (1 = ICO)
     53  view.setUint16(4, images.length, true); // idCount
     54 
     55  let dataOffset = metadataSize; // Append image data directly after the meta data.
     56  for (let i = 0; i < images.length; i++) {
     57    const off = ICO_HEADER_SIZE + ICO_DIR_ENTRY_SIZE * i;
     58 
     59    // We use a zero width and height because we always use compressed PNGs,
     60    // which require this and have their own width/height information.
     61    view.setUint8(off, 0); // bWidth
     62    view.setUint8(off + 1, 0); // bHeight
     63    view.setUint8(off + 2, 0); // bColorCount
     64    view.setUint8(off + 3, 0); // bReserved
     65    view.setUint16(off + 4, 1, true); // wPlanes
     66    view.setUint16(off + 6, 32, true); // wBitCount
     67    view.setUint32(off + 8, images[i].byteLength, true); // dwBytesInRes
     68    view.setUint32(off + 12, dataOffset, true); // dwImageOffset
     69 
     70    // Copy the image's bytes into the ICO buffer.
     71    u8.set(images[i], dataOffset);
     72 
     73    dataOffset += images[i].byteLength;
     74  }
     75 
     76  return buffer;
     77 }
     78 
     79 export class LinkHandlerParent extends JSWindowActorParent {
     80  static addListenerForTests(listener) {
     81    gTestListeners.add(listener);
     82  }
     83 
     84  static removeListenerForTests(listener) {
     85    gTestListeners.delete(listener);
     86  }
     87 
     88  receiveMessage(aMsg) {
     89    let browser = this.browsingContext.top.embedderElement;
     90    if (!browser) {
     91      return;
     92    }
     93 
     94    let win = browser.ownerGlobal;
     95 
     96    let gBrowser = win.gBrowser;
     97 
     98    switch (aMsg.name) {
     99      case "Link:LoadingIcon":
    100        if (!gBrowser) {
    101          return;
    102        }
    103 
    104        if (!aMsg.data.isRichIcon) {
    105          let tab = gBrowser.getTabForBrowser(browser);
    106          if (tab.hasAttribute("busy")) {
    107            tab.setAttribute("pendingicon", "true");
    108          }
    109        }
    110 
    111        this.notifyTestListeners("LoadingIcon", aMsg.data);
    112        break;
    113 
    114      case "Link:SetIcon":
    115        if (!gBrowser) {
    116          return;
    117        }
    118 
    119        this.setIconFromLink(gBrowser, browser, aMsg.data);
    120 
    121        this.notifyTestListeners("SetIcon", aMsg.data);
    122        break;
    123 
    124      case "Link:SetFailedIcon":
    125        if (!gBrowser) {
    126          return;
    127        }
    128 
    129        if (!aMsg.data.isRichIcon) {
    130          this.clearPendingIcon(gBrowser, browser);
    131        }
    132 
    133        this.notifyTestListeners("SetFailedIcon", aMsg.data);
    134        break;
    135 
    136      case "Link:AddSearch": {
    137        if (!gBrowser) {
    138          return;
    139        }
    140 
    141        let tab = gBrowser.getTabForBrowser(browser);
    142        if (!tab) {
    143          break;
    144        }
    145 
    146        lazy.OpenSearchManager.addEngine(browser, aMsg.data.engine);
    147        break;
    148      }
    149    }
    150  }
    151 
    152  notifyTestListeners(name, data) {
    153    for (let listener of gTestListeners) {
    154      listener(name, data);
    155    }
    156  }
    157 
    158  clearPendingIcon(gBrowser, aBrowser) {
    159    let tab = gBrowser.getTabForBrowser(aBrowser);
    160    tab.removeAttribute("pendingicon");
    161  }
    162 
    163  async setIconFromLink(
    164    gBrowser,
    165    browser,
    166    {
    167      pageURL,
    168      originalURL,
    169      expiration,
    170      iconURL,
    171      images,
    172      canStoreIcon,
    173      beforePageShow,
    174      isRichIcon,
    175    }
    176  ) {
    177    let tab = gBrowser.getTabForBrowser(browser);
    178    if (!tab) {
    179      return;
    180    }
    181 
    182    if (images) {
    183      let canvas = tab.ownerDocument.createElement("canvas");
    184 
    185      // We have multiple images, need to create an ICO file to collect them.
    186      if (images.length > 1) {
    187        // Convert all images to PNG bytes.
    188        let blobs = [];
    189        for (let image of images) {
    190          await drawImageOnCanvas(canvas, image);
    191          blobs.push(await new Promise(resolve => canvas.toBlob(resolve)));
    192        }
    193        let buffers = await Promise.all(blobs.map(blob => blob.bytes()));
    194 
    195        // Create an ICO "file" containing all the PNGs.
    196        let ico = createICO(buffers);
    197 
    198        // Convert the ICO bytes to a data URL.
    199        iconURL = await blobAsDataURL(new Blob([ico], { type: TYPE_ICO }));
    200      } else {
    201        await drawImageOnCanvas(canvas, images[0]);
    202        iconURL = canvas.toDataURL();
    203      }
    204    }
    205 
    206    // The browser might have gone away during `await` above.
    207    if (!gBrowser.getBrowserForTab(tab)) {
    208      return;
    209    }
    210 
    211    if (!isRichIcon) {
    212      this.clearPendingIcon(gBrowser, browser);
    213    }
    214 
    215    let iconURI;
    216    try {
    217      iconURI = Services.io.newURI(iconURL);
    218    } catch (ex) {
    219      console.error(ex);
    220      return;
    221    }
    222 
    223    // The content process should send decoded images for all schemes except for trusted schemes and SVGs, which should not be rasterized.
    224    if (
    225      !images &&
    226      !TRUSTED_FAVICON_SCHEMES.includes(iconURI.scheme) &&
    227      !iconURL.startsWith(SVG_DATA_URI_PREFIX)
    228    ) {
    229      console.error(
    230        `Not allowed to set favicon "${iconURL}" with that scheme!`
    231      );
    232      return;
    233    }
    234 
    235    if (!iconURI.schemeIs("data")) {
    236      try {
    237        Services.scriptSecurityManager.checkLoadURIWithPrincipal(
    238          browser.contentPrincipal,
    239          iconURI,
    240          Services.scriptSecurityManager.ALLOW_CHROME
    241        );
    242      } catch (ex) {
    243        return;
    244      }
    245    }
    246    if (canStoreIcon) {
    247      try {
    248        lazy.PlacesUtils.favicons
    249          .setFaviconForPage(
    250            Services.io.newURI(pageURL),
    251            Services.io.newURI(originalURL),
    252            iconURI,
    253            expiration && lazy.PlacesUtils.toPRTime(expiration),
    254            isRichIcon
    255          )
    256          .catch(console.error);
    257      } catch (ex) {
    258        console.error(ex);
    259      }
    260    }
    261 
    262    if (!isRichIcon) {
    263      gBrowser.setIcon(tab, iconURL, originalURL, beforePageShow);
    264    }
    265  }
    266 }