tor-browser

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

pageInfo.js (36732B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /* import-globals-from /toolkit/content/globalOverlay.js */
      6 /* import-globals-from /toolkit/content/contentAreaUtils.js */
      7 /* import-globals-from /toolkit/content/treeUtils.js */
      8 /* import-globals-from ../utilityOverlay.js */
      9 /* import-globals-from permissions.js */
     10 /* import-globals-from security.js */
     11 
     12 ChromeUtils.defineESModuleGetters(this, {
     13  E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
     14 });
     15 
     16 // define a js object to implement nsITreeView
     17 function pageInfoTreeView(treeid, copycol) {
     18  // copycol is the index number for the column that we want to add to
     19  // the copy-n-paste buffer when the user hits accel-c
     20  this.treeid = treeid;
     21  this.copycol = copycol;
     22  this.rows = 0;
     23  this.tree = null;
     24  this.data = [];
     25  this.selection = null;
     26  this.sortcol = -1;
     27  this.sortdir = false;
     28 }
     29 
     30 pageInfoTreeView.prototype = {
     31  set rowCount(c) {
     32    throw new Error("rowCount is a readonly property");
     33  },
     34  get rowCount() {
     35    return this.rows;
     36  },
     37 
     38  setTree(tree) {
     39    this.tree = tree;
     40  },
     41 
     42  getCellText(row, column) {
     43    // row can be null, but js arrays are 0-indexed.
     44    // colidx cannot be null, but can be larger than the number
     45    // of columns in the array. In this case it's the fault of
     46    // whoever typoed while calling this function.
     47    return this.data[row][column.index] || "";
     48  },
     49 
     50  setCellValue() {},
     51 
     52  setCellText(row, column, value) {
     53    this.data[row][column.index] = value;
     54  },
     55 
     56  addRow(row) {
     57    this.rows = this.data.push(row);
     58    this.rowCountChanged(this.rows - 1, 1);
     59    if (this.selection.count == 0 && this.rowCount && !gImageElement) {
     60      this.selection.select(0);
     61    }
     62  },
     63 
     64  addRows(rows) {
     65    for (let row of rows) {
     66      this.addRow(row);
     67    }
     68  },
     69 
     70  rowCountChanged(index, count) {
     71    this.tree.rowCountChanged(index, count);
     72  },
     73 
     74  invalidate() {
     75    this.tree.invalidate();
     76  },
     77 
     78  clear() {
     79    if (this.tree) {
     80      this.tree.rowCountChanged(0, -this.rows);
     81    }
     82    this.rows = 0;
     83    this.data = [];
     84  },
     85 
     86  onPageMediaSort(columnname) {
     87    var tree = document.getElementById(this.treeid);
     88    var treecol = tree.columns.getNamedColumn(columnname);
     89 
     90    this.sortdir = gTreeUtils.sort(
     91      tree,
     92      this,
     93      this.data,
     94      treecol.index,
     95      function textComparator(a, b) {
     96        return (a || "").toLowerCase().localeCompare((b || "").toLowerCase());
     97      },
     98      this.sortcol,
     99      this.sortdir
    100    );
    101 
    102    for (let col of tree.columns) {
    103      col.element.removeAttribute("sortActive");
    104      col.element.removeAttribute("sortDirection");
    105    }
    106    treecol.element.setAttribute("sortActive", "true");
    107    treecol.element.setAttribute(
    108      "sortDirection",
    109      this.sortdir ? "ascending" : "descending"
    110    );
    111 
    112    this.sortcol = treecol.index;
    113  },
    114 
    115  getRowProperties() {
    116    return "";
    117  },
    118  getCellProperties() {
    119    return "";
    120  },
    121  getColumnProperties() {
    122    return "";
    123  },
    124  isContainer() {
    125    return false;
    126  },
    127  isContainerOpen() {
    128    return false;
    129  },
    130  isSeparator() {
    131    return false;
    132  },
    133  isSorted() {
    134    return this.sortcol > -1;
    135  },
    136  canDrop() {
    137    return false;
    138  },
    139  drop() {
    140    return false;
    141  },
    142  getParentIndex() {
    143    return 0;
    144  },
    145  hasNextSibling() {
    146    return false;
    147  },
    148  getLevel() {
    149    return 0;
    150  },
    151  getImageSrc() {},
    152  getCellValue(row, column) {
    153    let col = column != null ? column : this.copycol;
    154    return row < 0 || col < 0 ? "" : this.data[row][col] || "";
    155  },
    156  toggleOpenState() {},
    157  cycleHeader() {},
    158  selectionChanged() {},
    159  cycleCell() {},
    160  isEditable() {
    161    return false;
    162  },
    163 };
    164 
    165 // mmm, yummy. global variables.
    166 var gDocInfo = null;
    167 var gImageElement = null;
    168 
    169 // column number to help using the data array
    170 const COL_IMAGE_ADDRESS = 0;
    171 const COL_IMAGE_TYPE = 1;
    172 const COL_IMAGE_SIZE = 2;
    173 const COL_IMAGE_ALT = 3;
    174 const COL_IMAGE_COUNT = 4;
    175 const COL_IMAGE_NODE = 5;
    176 const COL_IMAGE_BG = 6;
    177 const COL_IMAGE_RAWSIZE = 7;
    178 
    179 // column number to copy from, second argument to pageInfoTreeView's constructor
    180 const COPYCOL_NONE = -1;
    181 const COPYCOL_META_CONTENT = 1;
    182 const COPYCOL_IMAGE = COL_IMAGE_ADDRESS;
    183 
    184 // one nsITreeView for each tree in the window
    185 var gMetaView = new pageInfoTreeView("metatree", COPYCOL_META_CONTENT);
    186 var gImageView = new pageInfoTreeView("imagetree", COPYCOL_IMAGE);
    187 
    188 gImageView.getCellProperties = function (row, col) {
    189  var data = gImageView.data[row];
    190  var item = gImageView.data[row][COL_IMAGE_NODE];
    191  var props = "";
    192  if (
    193    !checkProtocol(data) ||
    194    HTMLEmbedElement.isInstance(item) ||
    195    (HTMLObjectElement.isInstance(item) && !item.type.startsWith("image/"))
    196  ) {
    197    props += "broken";
    198  }
    199 
    200  if (col.element.id == "image-address") {
    201    props += " ltr";
    202  }
    203 
    204  return props;
    205 };
    206 
    207 gImageView.onPageMediaSort = function (columnname) {
    208  var tree = document.getElementById(this.treeid);
    209  var treecol = tree.columns.getNamedColumn(columnname);
    210 
    211  var comparator;
    212  var index = treecol.index;
    213  if (index == COL_IMAGE_SIZE || index == COL_IMAGE_COUNT) {
    214    comparator = function numComparator(a, b) {
    215      return a - b;
    216    };
    217 
    218    // COL_IMAGE_SIZE contains the localized string, compare raw numbers.
    219    if (index == COL_IMAGE_SIZE) {
    220      index = COL_IMAGE_RAWSIZE;
    221    }
    222  } else {
    223    comparator = function textComparator(a, b) {
    224      return (a || "").toLowerCase().localeCompare((b || "").toLowerCase());
    225    };
    226  }
    227 
    228  this.sortdir = gTreeUtils.sort(
    229    tree,
    230    this,
    231    this.data,
    232    index,
    233    comparator,
    234    this.sortcol,
    235    this.sortdir
    236  );
    237 
    238  for (let col of tree.columns) {
    239    col.element.removeAttribute("sortActive");
    240    col.element.removeAttribute("sortDirection");
    241  }
    242  treecol.element.setAttribute("sortActive", "true");
    243  treecol.element.setAttribute(
    244    "sortDirection",
    245    this.sortdir ? "ascending" : "descending"
    246  );
    247 
    248  this.sortcol = index;
    249 };
    250 
    251 var gImageHash = {};
    252 
    253 // localized strings (will be filled in when the document is loaded)
    254 const MEDIA_STRINGS = {};
    255 let SIZE_UNKNOWN = "";
    256 let ALT_NOT_SET = "";
    257 
    258 // a number of services I'll need later
    259 // the cache services
    260 const nsICacheStorageService = Ci.nsICacheStorageService;
    261 const nsICacheStorage = Ci.nsICacheStorage;
    262 const cacheService = Cc[
    263  "@mozilla.org/netwerk/cache-storage-service;1"
    264 ].getService(nsICacheStorageService);
    265 
    266 var diskStorage = null;
    267 
    268 const nsICookiePermission = Ci.nsICookiePermission;
    269 
    270 const nsICertificateDialogs = Ci.nsICertificateDialogs;
    271 const CERTIFICATEDIALOGS_CONTRACTID = "@mozilla.org/nsCertificateDialogs;1";
    272 
    273 // clipboard helper
    274 function getClipboardHelper() {
    275  try {
    276    return Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
    277      Ci.nsIClipboardHelper
    278    );
    279  } catch (e) {
    280    // do nothing, later code will handle the error
    281    return null;
    282  }
    283 }
    284 const gClipboardHelper = getClipboardHelper();
    285 
    286 /* Called when PageInfo window is loaded.  Arguments are:
    287 *  window.arguments[0] - (optional) an object consisting of
    288 *                         - doc: (optional) document to use for source. if not provided,
    289 *                                the calling window's document will be used
    290 *                         - initialTab: (optional) id of the inital tab to display
    291 */
    292 window.addEventListener(
    293  "load",
    294  async function onLoadPageInfo() {
    295    [
    296      SIZE_UNKNOWN,
    297      ALT_NOT_SET,
    298      MEDIA_STRINGS.img,
    299      MEDIA_STRINGS["bg-img"],
    300      MEDIA_STRINGS["border-img"],
    301      MEDIA_STRINGS["list-img"],
    302      MEDIA_STRINGS.cursor,
    303      MEDIA_STRINGS.object,
    304      MEDIA_STRINGS.embed,
    305      MEDIA_STRINGS.link,
    306      MEDIA_STRINGS.input,
    307      MEDIA_STRINGS.video,
    308      MEDIA_STRINGS.audio,
    309    ] = await document.l10n.formatValues([
    310      "image-size-unknown",
    311      "not-set-alternative-text",
    312      "media-img",
    313      "media-bg-img",
    314      "media-border-img",
    315      "media-list-img",
    316      "media-cursor",
    317      "media-object",
    318      "media-embed",
    319      "media-link",
    320      "media-input",
    321      "media-video",
    322      "media-audio",
    323    ]);
    324 
    325    const args =
    326      "arguments" in window &&
    327      window.arguments.length >= 1 &&
    328      window.arguments[0];
    329 
    330    // Init media view
    331    let imageTree = document.getElementById("imagetree");
    332    imageTree.view = gImageView;
    333 
    334    imageTree.controllers.appendController(treeController);
    335 
    336    document
    337      .getElementById("metatree")
    338      .controllers.appendController(treeController);
    339 
    340    document
    341      .querySelector("#metatree > treecols")
    342      .addEventListener("click", event => {
    343        let id = event.target.id;
    344        switch (id) {
    345          case "meta-name":
    346          case "meta-content":
    347            gMetaView.onPageMediaSort(id);
    348            break;
    349        }
    350      });
    351 
    352    document
    353      .querySelector("#imagetree > treecols")
    354      .addEventListener("click", event => {
    355        let id = event.target.id;
    356        switch (id) {
    357          case "image-address":
    358          case "image-type":
    359          case "image-size":
    360          case "image-alt":
    361          case "image-count":
    362            gImageView.onPageMediaSort(id);
    363            break;
    364        }
    365      });
    366 
    367    let imagetree = document.getElementById("imagetree");
    368    imagetree.addEventListener("select", onImageSelect);
    369    imagetree.addEventListener("dragstart", event =>
    370      onBeginLinkDrag(event, "image-address", "image-alt")
    371    );
    372 
    373    document.addEventListener("command", event => {
    374      switch (event.target.id) {
    375        // == pageInfoCommandSet ==
    376        case "cmd_close":
    377          window.close();
    378          break;
    379        case "cmd_help":
    380          doHelpButton();
    381          break;
    382        // == topBar ==
    383        case "generalTab":
    384        case "mediaTab":
    385        case "permTab":
    386        case "securityTab":
    387          showTab(event.target.id.slice(0, -3));
    388          break;
    389        // == imageSaveBox ==
    390        case "selectallbutton":
    391          doSelectAllMedia();
    392          break;
    393        case "imagesaveasbutton":
    394        case "mediasaveasbutton":
    395          saveMedia();
    396          break;
    397        // == securityPanel ==
    398        case "security-view-cert":
    399          security.viewCert();
    400          break;
    401        case "security-clear-sitedata":
    402          security.clearSiteData();
    403          break;
    404        case "security-view-password":
    405          security.viewPasswords();
    406          break;
    407      }
    408    });
    409 
    410    // Select the requested tab, if the name is specified
    411    await loadTab(args);
    412 
    413    // Emit init event for tests
    414    window.dispatchEvent(new Event("page-info-init"));
    415  },
    416  { once: true }
    417 );
    418 
    419 async function loadPageInfo(browsingContext, imageElement, browser) {
    420  browser = browser || window.opener.gBrowser.selectedBrowser;
    421  browsingContext = browsingContext || browser.browsingContext;
    422 
    423  let actor = browsingContext.currentWindowGlobal.getActor("PageInfo");
    424 
    425  let result = await actor.sendQuery("PageInfo:getData");
    426  await onNonMediaPageInfoLoad(browser, result, imageElement);
    427 
    428  // Here, we are walking the frame tree via BrowsingContexts to collect all of the
    429  // media information for each frame
    430  let contextsToVisit = [browsingContext];
    431  while (contextsToVisit.length) {
    432    let currContext = contextsToVisit.pop();
    433    let global = currContext.currentWindowGlobal;
    434 
    435    if (!global) {
    436      continue;
    437    }
    438 
    439    let subframeActor = global.getActor("PageInfo");
    440    let mediaResult = await subframeActor.sendQuery("PageInfo:getMediaData");
    441    for (let item of mediaResult.mediaItems) {
    442      addImage(item);
    443    }
    444    selectImage();
    445    contextsToVisit.push(...currContext.children);
    446  }
    447 }
    448 
    449 // Setup the <browser> used for media previews
    450 function createPreviewBrowserElement(browser, docInfo) {
    451  const previewBrowser = document.createXULElement("browser");
    452  previewBrowser.setAttribute("id", "mediaBrowser");
    453  previewBrowser.setAttribute("type", "content");
    454  previewBrowser.setAttribute("remote", "true");
    455  previewBrowser.setAttribute("remoteType", browser.remoteType);
    456  previewBrowser.setAttribute("maychangeremoteness", "true");
    457  previewBrowser.setAttribute("disableglobalhistory", "true");
    458  previewBrowser.setAttribute("nodefaultsrc", "true");
    459  previewBrowser.setAttribute("disablecontextmenu", "true");
    460  previewBrowser.setAttribute(
    461    "initialBrowsingContextGroupId",
    462    browser.browsingContext.group.id
    463  );
    464 
    465  let { userContextId } = docInfo.principal.originAttributes;
    466  if (userContextId) {
    467    previewBrowser.setAttribute("usercontextid", userContextId);
    468  }
    469 
    470  document.getElementById("mediaBrowser").replaceWith(previewBrowser);
    471 }
    472 
    473 /**
    474 * onNonMediaPageInfoLoad is responsible for populating the page info
    475 * UI other than the media tab. This includes general, permissions, and security.
    476 */
    477 async function onNonMediaPageInfoLoad(browser, pageInfoData, imageInfo) {
    478  const { docInfo, windowInfo } = pageInfoData;
    479  let uri = Services.io.newURI(docInfo.documentURIObject.spec);
    480  let principal = docInfo.principal;
    481  gDocInfo = docInfo;
    482 
    483  gImageElement = imageInfo;
    484  var titleFormat = windowInfo.isTopWindow
    485    ? "page-info-page"
    486    : "page-info-frame";
    487  document.l10n.setAttributes(document.documentElement, titleFormat, {
    488    website: docInfo.location,
    489  });
    490 
    491  document
    492    .getElementById("main-window")
    493    .setAttribute("relatedUrl", docInfo.location);
    494 
    495  createPreviewBrowserElement(browser, docInfo);
    496 
    497  await makeGeneralTab(pageInfoData.metaViewRows, docInfo);
    498  if (
    499    uri.spec.startsWith("about:neterror") ||
    500    uri.spec.startsWith("about:certerror") ||
    501    uri.spec.startsWith("about:httpsonlyerror")
    502  ) {
    503    uri = browser.currentURI;
    504    principal = Services.scriptSecurityManager.createContentPrincipal(
    505      uri,
    506      browser.contentPrincipal.originAttributes
    507    );
    508  }
    509  onLoadPermission(uri, principal);
    510  securityOnLoad(uri, windowInfo);
    511 }
    512 
    513 function resetPageInfo(args) {
    514  /* Reset Meta tags part */
    515  gMetaView.clear();
    516 
    517  /* Reset Media tab */
    518  var mediaTab = document.getElementById("mediaTab");
    519  if (!mediaTab.hidden) {
    520    mediaTab.hidden = true;
    521  }
    522  gImageView.clear();
    523  gImageHash = {};
    524 
    525  /* Rebuild the data */
    526  loadTab(args);
    527 }
    528 
    529 function doHelpButton() {
    530  const helpTopics = {
    531    generalPanel: "pageinfo_general",
    532    mediaPanel: "pageinfo_media",
    533    permPanel: "pageinfo_permissions",
    534    securityPanel: "pageinfo_security",
    535  };
    536 
    537  var deck = document.getElementById("mainDeck");
    538  var helpdoc = helpTopics[deck.selectedPanel.id] || "pageinfo_general";
    539  openHelpLink(helpdoc);
    540 }
    541 
    542 function showTab(id) {
    543  var deck = document.getElementById("mainDeck");
    544  var pagel = document.getElementById(id + "Panel");
    545  deck.selectedPanel = pagel;
    546 }
    547 
    548 async function loadTab(args) {
    549  // If the "View Image Info" context menu item was used, the related image
    550  // element is provided as an argument. This can't be a background image.
    551  let imageElement = args?.imageElement;
    552  let browsingContext = args?.browsingContext;
    553  let browser = args?.browser;
    554 
    555  // Check if diskStorage has not be created yet if it has not been, get
    556  // partitionKey from content process and create diskStorage with said partitionKey
    557  if (!diskStorage) {
    558    let oaWithPartitionKey = await getOaWithPartitionKey(
    559      browsingContext,
    560      browser
    561    );
    562    let loadContextInfo = Services.loadContextInfo.custom(
    563      false,
    564      oaWithPartitionKey
    565    );
    566    diskStorage = cacheService.diskCacheStorage(loadContextInfo);
    567  }
    568 
    569  /* Load the page info */
    570  await loadPageInfo(browsingContext, imageElement, browser);
    571 
    572  var initialTab = args?.initialTab || "generalTab";
    573  var radioGroup = document.getElementById("viewGroup");
    574  initialTab =
    575    document.getElementById(initialTab) ||
    576    document.getElementById("generalTab");
    577  radioGroup.selectedItem = initialTab;
    578  radioGroup.selectedItem.doCommand();
    579  radioGroup.focus({ focusVisible: false });
    580 }
    581 
    582 function openCacheEntry(key, cb) {
    583  var checkCacheListener = {
    584    onCacheEntryCheck() {
    585      return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
    586    },
    587    onCacheEntryAvailable(entry) {
    588      cb(entry);
    589    },
    590  };
    591  diskStorage.asyncOpenURI(
    592    Services.io.newURI(key),
    593    "",
    594    nsICacheStorage.OPEN_READONLY,
    595    checkCacheListener
    596  );
    597 }
    598 
    599 async function makeGeneralTab(metaViewRows, docInfo) {
    600  // Sets Title in the General Tab, set to "Untitled Page" if no title found
    601  if (docInfo.title) {
    602    document.getElementById("titletext").value = docInfo.title;
    603  } else {
    604    document.l10n.setAttributes(
    605      document.getElementById("titletext"),
    606      "no-page-title"
    607    );
    608  }
    609 
    610  var url = docInfo.location;
    611  setItemValue("urltext", url);
    612 
    613  var referrer = "referrer" in docInfo && docInfo.referrer;
    614  setItemValue("refertext", referrer);
    615 
    616  var mode =
    617    "compatMode" in docInfo && docInfo.compatMode == "BackCompat"
    618      ? "general-quirks-mode"
    619      : "general-strict-mode";
    620  document.l10n.setAttributes(document.getElementById("modetext"), mode);
    621 
    622  // find out the mime type
    623  setItemValue("typetext", docInfo.contentType);
    624 
    625  // get the document characterset
    626  var encoding = docInfo.characterSet;
    627  document.getElementById("encodingtext").value = encoding;
    628 
    629  let length = metaViewRows.length;
    630 
    631  var metaGroup = document.getElementById("metaTags");
    632  if (!length) {
    633    metaGroup.style.visibility = "hidden";
    634  } else {
    635    document.l10n.setAttributes(
    636      document.getElementById("metaTagsCaption"),
    637      "general-meta-tags",
    638      { tags: length }
    639    );
    640 
    641    document.getElementById("metatree").view = gMetaView;
    642 
    643    // Add the metaViewRows onto the general tab's meta info tree.
    644    gMetaView.addRows(metaViewRows);
    645 
    646    metaGroup.style.removeProperty("visibility");
    647  }
    648 
    649  var modifiedText = formatDate(
    650    docInfo.lastModified,
    651    await document.l10n.formatValue("not-set-date")
    652  );
    653  document.getElementById("modifiedtext").value = modifiedText;
    654 
    655  // get cache info
    656  var cacheKey = url.replace(/#.*$/, "");
    657  openCacheEntry(cacheKey, function (cacheEntry) {
    658    if (cacheEntry) {
    659      var pageSize = cacheEntry.dataSize;
    660      var kbSize = formatNumber(Math.round((pageSize / 1024) * 100) / 100);
    661      document.l10n.setAttributes(
    662        document.getElementById("sizetext"),
    663        "properties-general-size",
    664        { kb: kbSize, bytes: formatNumber(pageSize) }
    665      );
    666    } else {
    667      setItemValue("sizetext", null);
    668    }
    669  });
    670 }
    671 
    672 async function addImage({ url, type, alt, altNotProvided, element, isBg }) {
    673  if (!url) {
    674    return;
    675  }
    676 
    677  if (altNotProvided) {
    678    alt = ALT_NOT_SET;
    679  }
    680 
    681  if (!gImageHash.hasOwnProperty(url)) {
    682    gImageHash[url] = {};
    683  }
    684  if (!gImageHash[url].hasOwnProperty(type)) {
    685    gImageHash[url][type] = {};
    686  }
    687  if (!gImageHash[url][type].hasOwnProperty(alt)) {
    688    gImageHash[url][type][alt] = gImageView.data.length;
    689    var row = [
    690      url,
    691      MEDIA_STRINGS[type],
    692      SIZE_UNKNOWN,
    693      alt,
    694      1,
    695      element,
    696      isBg,
    697      -1,
    698    ];
    699    gImageView.addRow(row);
    700 
    701    // Fill in cache data asynchronously
    702    openCacheEntry(url, function (cacheEntry) {
    703      if (cacheEntry) {
    704        let value = cacheEntry.dataSize;
    705        // If value is not -1 then replace with actual value, else keep as "unknown"
    706        if (value != -1) {
    707          row[COL_IMAGE_RAWSIZE] = value;
    708          let kbSize = Number(Math.round((value / 1024) * 100) / 100);
    709          document.l10n
    710            .formatValue("media-file-size", { size: kbSize })
    711            .then(function (response) {
    712              row[COL_IMAGE_SIZE] = response;
    713              // Invalidate the row to trigger a repaint.
    714              gImageView.tree.invalidateRow(gImageView.data.indexOf(row));
    715            });
    716        }
    717      }
    718    });
    719 
    720    if (gImageView.data.length == 1) {
    721      document.getElementById("mediaTab").hidden = false;
    722    }
    723  } else {
    724    var i = gImageHash[url][type][alt];
    725    gImageView.data[i][COL_IMAGE_COUNT]++;
    726    // The same image can occur several times on the page at different sizes.
    727    // If the "View Image Info" context menu item was used, ensure we select
    728    // the correct element.
    729    if (
    730      !gImageView.data[i][COL_IMAGE_BG] &&
    731      gImageElement &&
    732      url == gImageElement.currentSrc &&
    733      gImageElement.width == element.width &&
    734      gImageElement.height == element.height &&
    735      gImageElement.imageText == element.imageText
    736    ) {
    737      gImageView.data[i][COL_IMAGE_NODE] = element;
    738    }
    739  }
    740 }
    741 
    742 // Link Stuff
    743 function onBeginLinkDrag(event, urlField, descField) {
    744  if (event.originalTarget.localName != "treechildren") {
    745    return;
    746  }
    747 
    748  var tree = event.target;
    749  if (tree.localName != "tree") {
    750    tree = tree.parentNode;
    751  }
    752 
    753  var row = tree.getRowAt(event.clientX, event.clientY);
    754  if (row == -1) {
    755    return;
    756  }
    757 
    758  // Adding URL flavor
    759  var col = tree.columns[urlField];
    760  var url = tree.view.getCellText(row, col);
    761  col = tree.columns[descField];
    762  var desc = tree.view.getCellText(row, col);
    763 
    764  var dt = event.dataTransfer;
    765  dt.setData("text/x-moz-url", url + "\n" + desc);
    766  dt.setData("text/url-list", url);
    767  dt.setData("text/plain", url);
    768 }
    769 
    770 // Image Stuff
    771 function getSelectedRows(tree) {
    772  var start = {};
    773  var end = {};
    774  var numRanges = tree.view.selection.getRangeCount();
    775 
    776  var rowArray = [];
    777  for (var t = 0; t < numRanges; t++) {
    778    tree.view.selection.getRangeAt(t, start, end);
    779    for (var v = start.value; v <= end.value; v++) {
    780      rowArray.push(v);
    781    }
    782  }
    783 
    784  return rowArray;
    785 }
    786 
    787 function getSelectedRow(tree) {
    788  var rows = getSelectedRows(tree);
    789  return rows.length == 1 ? rows[0] : -1;
    790 }
    791 
    792 async function selectSaveFolder(aCallback) {
    793  const { nsIFile, nsIFilePicker } = Ci;
    794  let titleText = await document.l10n.formatValue("media-select-folder");
    795  let fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
    796  let fpCallback = function fpCallback_done(aResult) {
    797    if (aResult == nsIFilePicker.returnOK) {
    798      aCallback(fp.file.QueryInterface(nsIFile));
    799    } else {
    800      aCallback(null);
    801    }
    802  };
    803 
    804  fp.init(window.browsingContext, titleText, nsIFilePicker.modeGetFolder);
    805  fp.appendFilters(nsIFilePicker.filterAll);
    806  try {
    807    let initialDir = Services.prefs.getComplexValue(
    808      "browser.download.dir",
    809      nsIFile
    810    );
    811    if (initialDir) {
    812      fp.displayDirectory = initialDir;
    813    }
    814  } catch (ex) {}
    815  fp.open(fpCallback);
    816 }
    817 
    818 function saveMedia() {
    819  var tree = document.getElementById("imagetree");
    820  var rowArray = getSelectedRows(tree);
    821  let ReferrerInfo = Components.Constructor(
    822    "@mozilla.org/referrer-info;1",
    823    "nsIReferrerInfo",
    824    "init"
    825  );
    826 
    827  if (rowArray.length == 1) {
    828    let row = rowArray[0];
    829    let item = gImageView.data[row][COL_IMAGE_NODE];
    830    let url = gImageView.data[row][COL_IMAGE_ADDRESS];
    831 
    832    if (url) {
    833      var titleKey = "SaveImageTitle";
    834 
    835      if (HTMLVideoElement.isInstance(item)) {
    836        titleKey = "SaveVideoTitle";
    837      } else if (HTMLAudioElement.isInstance(item)) {
    838        titleKey = "SaveAudioTitle";
    839      }
    840 
    841      // Bug 1565216 to evaluate passing referrer as item.baseURL
    842      let referrerInfo = new ReferrerInfo(
    843        Ci.nsIReferrerInfo.EMPTY,
    844        true,
    845        Services.io.newURI(item.baseURI)
    846      );
    847      let cookieJarSettings = E10SUtils.deserializeCookieJarSettings(
    848        gDocInfo.cookieJarSettings
    849      );
    850      internalSave(
    851        url,
    852        null,
    853        null,
    854        null,
    855        null,
    856        item.mimeType,
    857        false,
    858        titleKey,
    859        null,
    860        referrerInfo,
    861        cookieJarSettings,
    862        null,
    863        false,
    864        null,
    865        gDocInfo.isContentWindowPrivate,
    866        gDocInfo.principal
    867      );
    868    }
    869  } else {
    870    selectSaveFolder(function (aDirectory) {
    871      if (aDirectory) {
    872        var saveAnImage = function (aURIString, aChosenData, aBaseURI) {
    873          uniqueFile(aChosenData.file);
    874 
    875          let referrerInfo = new ReferrerInfo(
    876            Ci.nsIReferrerInfo.EMPTY,
    877            true,
    878            aBaseURI
    879          );
    880          let cookieJarSettings = E10SUtils.deserializeCookieJarSettings(
    881            gDocInfo.cookieJarSettings
    882          );
    883          internalSave(
    884            aURIString,
    885            null,
    886            null,
    887            null,
    888            null,
    889            null,
    890            false,
    891            "SaveImageTitle",
    892            aChosenData,
    893            referrerInfo,
    894            cookieJarSettings,
    895            null,
    896            false,
    897            null,
    898            gDocInfo.isContentWindowPrivate,
    899            gDocInfo.principal
    900          );
    901        };
    902 
    903        for (var i = 0; i < rowArray.length; i++) {
    904          let v = rowArray[i];
    905          let dir = aDirectory.clone();
    906          let item = gImageView.data[v][COL_IMAGE_NODE];
    907          let uriString = gImageView.data[v][COL_IMAGE_ADDRESS];
    908          let uri = Services.io.newURI(uriString);
    909 
    910          try {
    911            uri.QueryInterface(Ci.nsIURL);
    912            dir.append(decodeURIComponent(uri.fileName));
    913          } catch (ex) {
    914            // data:/blob: uris
    915            // Supply a dummy filename, otherwise Download Manager
    916            // will try to delete the base directory on failure.
    917            dir.append(gImageView.data[v][COL_IMAGE_TYPE]);
    918          }
    919 
    920          if (i == 0) {
    921            saveAnImage(
    922              uriString,
    923              new AutoChosen(dir, uri),
    924              Services.io.newURI(item.baseURI)
    925            );
    926          } else {
    927            // This delay is a hack which prevents the download manager
    928            // from opening many times. See bug 377339.
    929            setTimeout(
    930              saveAnImage,
    931              200,
    932              uriString,
    933              new AutoChosen(dir, uri),
    934              Services.io.newURI(item.baseURI)
    935            );
    936          }
    937        }
    938      }
    939    });
    940  }
    941 }
    942 
    943 function onImageSelect() {
    944  var previewBox = document.getElementById("mediaPreviewBox");
    945  var mediaSaveBox = document.getElementById("mediaSaveBox");
    946  var splitter = document.getElementById("mediaSplitter");
    947  var tree = document.getElementById("imagetree");
    948  var count = tree.view.selection.count;
    949  if (count == 0) {
    950    previewBox.collapsed = true;
    951    mediaSaveBox.collapsed = true;
    952    splitter.collapsed = true;
    953    tree.setAttribute("flex", "1");
    954  } else if (count > 1) {
    955    splitter.collapsed = true;
    956    previewBox.collapsed = true;
    957    mediaSaveBox.collapsed = false;
    958    tree.setAttribute("flex", "1");
    959  } else {
    960    mediaSaveBox.collapsed = true;
    961    splitter.collapsed = false;
    962    previewBox.collapsed = false;
    963    tree.setAttribute("flex", "0");
    964    makePreview(getSelectedRows(tree)[0]);
    965  }
    966 }
    967 
    968 // Makes the media preview (image, video, etc) for the selected row on the media tab.
    969 function makePreview(row) {
    970  var item = gImageView.data[row][COL_IMAGE_NODE];
    971  var url = gImageView.data[row][COL_IMAGE_ADDRESS];
    972  var isBG = gImageView.data[row][COL_IMAGE_BG];
    973 
    974  setItemValue("imageurltext", url);
    975  setItemValue("imagetext", item.imageText);
    976  setItemValue("imagelongdesctext", item.longDesc);
    977 
    978  // get cache info
    979  var cacheKey = url.replace(/#.*$/, "");
    980  openCacheEntry(cacheKey, async function (cacheEntry) {
    981    // find out the file size
    982    if (cacheEntry) {
    983      let imageSize = cacheEntry.dataSize;
    984      var kbSize = Math.round((imageSize / 1024) * 100) / 100;
    985      document.l10n.setAttributes(
    986        document.getElementById("imagesizetext"),
    987        "properties-general-size",
    988        { kb: formatNumber(kbSize), bytes: formatNumber(imageSize) }
    989      );
    990    } else {
    991      document.l10n.setAttributes(
    992        document.getElementById("imagesizetext"),
    993        "media-unknown-not-cached"
    994      );
    995    }
    996 
    997    var mimeType = item.mimeType || this.getContentTypeFromHeaders(cacheEntry);
    998    var numFrames = item.numFrames;
    999 
   1000    let element = document.getElementById("imagetypetext");
   1001    var imageType;
   1002    if (mimeType) {
   1003      // We found the type, try to display it nicely
   1004      let imageMimeType = /^image\/(.*)/i.exec(mimeType);
   1005      if (imageMimeType) {
   1006        imageType = imageMimeType[1].toUpperCase();
   1007        if (numFrames > 1) {
   1008          document.l10n.setAttributes(element, "media-animated-image-type", {
   1009            type: imageType,
   1010            frames: numFrames,
   1011          });
   1012        } else {
   1013          document.l10n.setAttributes(element, "media-image-type", {
   1014            type: imageType,
   1015          });
   1016        }
   1017      } else {
   1018        // the MIME type doesn't begin with image/, display the raw type
   1019        element.setAttribute("value", mimeType);
   1020        element.removeAttribute("data-l10n-id");
   1021      }
   1022    } else {
   1023      // We couldn't find the type, fall back to the value in the treeview
   1024      element.setAttribute("value", gImageView.data[row][COL_IMAGE_TYPE]);
   1025      element.removeAttribute("data-l10n-id");
   1026    }
   1027 
   1028    let forceMediaDocument = null;
   1029    let message = {
   1030      width: undefined,
   1031      height: undefined,
   1032    };
   1033 
   1034    let isAllowed = checkProtocol(gImageView.data[row]);
   1035    if (isAllowed) {
   1036      try {
   1037        Services.scriptSecurityManager.checkLoadURIWithPrincipal(
   1038          gDocInfo.principal,
   1039          Services.io.newURI(url),
   1040          0
   1041        );
   1042      } catch {
   1043        isAllowed = false;
   1044      }
   1045    }
   1046 
   1047    if (
   1048      (item.HTMLLinkElement ||
   1049        item.HTMLInputElement ||
   1050        item.HTMLImageElement ||
   1051        item.SVGImageElement ||
   1052        (item.HTMLObjectElement && mimeType && mimeType.startsWith("image/")) ||
   1053        isBG) &&
   1054      isAllowed
   1055    ) {
   1056      forceMediaDocument = "image";
   1057 
   1058      if (item.SVGImageElement) {
   1059        message.width = item.SVGImageElementWidth;
   1060        message.height = item.SVGImageElementHeight;
   1061      } else if (!isBG) {
   1062        if ("width" in item && item.width) {
   1063          message.width = item.width;
   1064        }
   1065        if ("height" in item && item.height) {
   1066          message.height = item.height;
   1067        }
   1068      }
   1069 
   1070      document.getElementById("theimagecontainer").collapsed = false;
   1071      document.getElementById("brokenimagecontainer").collapsed = true;
   1072    } else if (item.HTMLVideoElement && isAllowed) {
   1073      forceMediaDocument = "video";
   1074 
   1075      document.getElementById("theimagecontainer").collapsed = false;
   1076      document.getElementById("brokenimagecontainer").collapsed = true;
   1077 
   1078      document.l10n.setAttributes(
   1079        document.getElementById("imagedimensiontext"),
   1080        "media-dimensions",
   1081        {
   1082          dimx: formatNumber(item.videoWidth),
   1083          dimy: formatNumber(item.videoHeight),
   1084        }
   1085      );
   1086    } else if (item.HTMLAudioElement && isAllowed) {
   1087      forceMediaDocument = "video"; // Audio also uses a VideoDocument.
   1088 
   1089      document.getElementById("theimagecontainer").collapsed = false;
   1090      document.getElementById("brokenimagecontainer").collapsed = true;
   1091    } else {
   1092      // fallback image for protocols not allowed (e.g., javascript:)
   1093      // or elements not [yet] handled (e.g., object, embed).
   1094      document.getElementById("brokenimagecontainer").collapsed = false;
   1095      document.getElementById("theimagecontainer").collapsed = true;
   1096      return;
   1097    }
   1098 
   1099    const mediaBrowser = document.getElementById("mediaBrowser");
   1100 
   1101    const options = {
   1102      triggeringPrincipal: gDocInfo.principal,
   1103      forceMediaDocument,
   1104    };
   1105    mediaBrowser.loadURI(Services.io.newURI(url), options);
   1106 
   1107    await new Promise(resolve => {
   1108      let webProgressListener = {
   1109        onStateChange(webProgress, request, aStateFlags) {
   1110          if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
   1111            mediaBrowser.webProgress?.removeProgressListener(
   1112              webProgressListener
   1113            );
   1114            resolve();
   1115          }
   1116        },
   1117 
   1118        QueryInterface: ChromeUtils.generateQI([
   1119          "nsIWebProgressListener2",
   1120          "nsIWebProgressListener",
   1121          "nsISupportsWeakReference",
   1122        ]),
   1123      };
   1124      mediaBrowser.addProgressListener(
   1125        webProgressListener,
   1126        Ci.nsIWebProgress.NOTIFY_STATE_WINDOW
   1127      );
   1128    });
   1129 
   1130    try {
   1131      const actor =
   1132        mediaBrowser.browsingContext.currentWindowGlobal.getActor(
   1133          "PageInfoPreview"
   1134        );
   1135 
   1136      let data = await actor.sendQuery("PageInfoPreview:resize", message);
   1137      if (!data) {
   1138        return;
   1139      }
   1140 
   1141      let tree = document.getElementById("imagetree");
   1142      let activeRow = getSelectedRows(tree)[0];
   1143 
   1144      // Make sure we only update the dimensions if the
   1145      // image is still selected.
   1146      if (url !== gImageView.data[activeRow][COL_IMAGE_ADDRESS]) {
   1147        return;
   1148      }
   1149 
   1150      if (
   1151        data.width != data.naturalWidth ||
   1152        data.height != data.naturalHeight
   1153      ) {
   1154        document.l10n.setAttributes(
   1155          document.getElementById("imagedimensiontext"),
   1156          "media-dimensions-scaled",
   1157          {
   1158            dimx: formatNumber(data.naturalWidth),
   1159            dimy: formatNumber(data.naturalHeight),
   1160            scaledx: formatNumber(data.width),
   1161            scaledy: formatNumber(data.height),
   1162          }
   1163        );
   1164      } else {
   1165        document.l10n.setAttributes(
   1166          document.getElementById("imagedimensiontext"),
   1167          "media-dimensions",
   1168          {
   1169            dimx: formatNumber(data.width),
   1170            dimy: formatNumber(data.height),
   1171          }
   1172        );
   1173      }
   1174    } catch (e) {
   1175      console.error(e);
   1176    } finally {
   1177      // Event for tests.
   1178      window.dispatchEvent(new Event("page-info-mediapreview-load"));
   1179    }
   1180  });
   1181 }
   1182 
   1183 function getContentTypeFromHeaders(cacheEntryDescriptor) {
   1184  if (!cacheEntryDescriptor) {
   1185    return null;
   1186  }
   1187 
   1188  let headers = cacheEntryDescriptor.getMetaDataElement("response-head");
   1189  let type = /^Content-Type:\s*(.*?)\s*(?:\;|$)/im.exec(headers);
   1190  return type && type[1];
   1191 }
   1192 
   1193 function setItemValue(id, value) {
   1194  var item = document.getElementById(id);
   1195  item.closest("tr").hidden = !value;
   1196  if (value) {
   1197    item.value = value;
   1198  }
   1199 }
   1200 
   1201 function formatNumber(number) {
   1202  return (+number).toLocaleString(); // coerce number to a numeric value before calling toLocaleString()
   1203 }
   1204 
   1205 function formatDate(datestr, unknown) {
   1206  var date = new Date(datestr);
   1207  if (!date.valueOf()) {
   1208    return unknown;
   1209  }
   1210 
   1211  const dateTimeFormatter = new Services.intl.DateTimeFormat(undefined, {
   1212    dateStyle: "long",
   1213    timeStyle: "long",
   1214  });
   1215  return dateTimeFormatter.format(date);
   1216 }
   1217 
   1218 let treeController = {
   1219  supportsCommand(command) {
   1220    return command == "cmd_copy" || command == "cmd_selectAll";
   1221  },
   1222 
   1223  isCommandEnabled() {
   1224    return true; // not worth checking for this
   1225  },
   1226 
   1227  doCommand(command) {
   1228    switch (command) {
   1229      case "cmd_copy":
   1230        doCopy();
   1231        break;
   1232      case "cmd_selectAll":
   1233        document.activeElement.view.selection.selectAll();
   1234        break;
   1235    }
   1236  },
   1237 };
   1238 
   1239 function doCopy() {
   1240  if (!gClipboardHelper) {
   1241    return;
   1242  }
   1243 
   1244  var elem = document.commandDispatcher.focusedElement;
   1245 
   1246  if (elem && elem.localName == "tree") {
   1247    var view = elem.view;
   1248    var selection = view.selection;
   1249    var text = [],
   1250      tmp = "";
   1251    var min = {},
   1252      max = {};
   1253 
   1254    var count = selection.getRangeCount();
   1255 
   1256    for (var i = 0; i < count; i++) {
   1257      selection.getRangeAt(i, min, max);
   1258 
   1259      for (var row = min.value; row <= max.value; row++) {
   1260        tmp = view.getCellValue(row, null);
   1261        if (tmp) {
   1262          text.push(tmp);
   1263        }
   1264      }
   1265    }
   1266    gClipboardHelper.copyString(text.join("\n"));
   1267  }
   1268 }
   1269 
   1270 function doSelectAllMedia() {
   1271  var tree = document.getElementById("imagetree");
   1272 
   1273  if (tree) {
   1274    tree.view.selection.selectAll();
   1275  }
   1276 }
   1277 
   1278 function selectImage() {
   1279  if (!gImageElement) {
   1280    return;
   1281  }
   1282 
   1283  var tree = document.getElementById("imagetree");
   1284  for (var i = 0; i < tree.view.rowCount; i++) {
   1285    // If the image row element is the image selected from the "View Image Info" context menu item.
   1286    let image = gImageView.data[i][COL_IMAGE_NODE];
   1287    if (
   1288      !gImageView.data[i][COL_IMAGE_BG] &&
   1289      gImageElement.currentSrc == gImageView.data[i][COL_IMAGE_ADDRESS] &&
   1290      gImageElement.width == image.width &&
   1291      gImageElement.height == image.height &&
   1292      gImageElement.imageText == image.imageText
   1293    ) {
   1294      tree.view.selection.select(i);
   1295      tree.ensureRowIsVisible(i);
   1296      tree.focus();
   1297      return;
   1298    }
   1299  }
   1300 }
   1301 
   1302 function checkProtocol(img) {
   1303  var url = img[COL_IMAGE_ADDRESS];
   1304  return (
   1305    /^data:image\//i.test(url) ||
   1306    /^(https?|file|about|chrome|resource):/.test(url)
   1307  );
   1308 }
   1309 
   1310 async function getOaWithPartitionKey(browsingContext, browser) {
   1311  browser = browser || window.opener.gBrowser.selectedBrowser;
   1312  browsingContext = browsingContext || browser.browsingContext;
   1313 
   1314  let actor = browsingContext.currentWindowGlobal.getActor("PageInfo");
   1315  let partitionKeyFromChild = await actor.sendQuery("PageInfo:getPartitionKey");
   1316 
   1317  let oa = browser.contentPrincipal.originAttributes;
   1318  oa.partitionKey = partitionKeyFromChild.partitionKey;
   1319 
   1320  return oa;
   1321 }