forked from mirrors/gecko-dev
The changes in pageInfo.js is just trying to keep the existing behavior where the event listener is handling both the successful and failing loads. Based on the code the listener may not be designed to handle error case, but that is what it does currently. Depends on D159929 Differential Revision: https://phabricator.services.mozilla.com/D159930
1171 lines
33 KiB
JavaScript
1171 lines
33 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/. */
|
|
|
|
/* import-globals-from /toolkit/content/globalOverlay.js */
|
|
/* import-globals-from /toolkit/content/contentAreaUtils.js */
|
|
/* import-globals-from /toolkit/content/treeUtils.js */
|
|
/* import-globals-from ../utilityOverlay.js */
|
|
/* import-globals-from permissions.js */
|
|
/* import-globals-from security.js */
|
|
|
|
ChromeUtils.defineESModuleGetters(this, {
|
|
E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
|
|
});
|
|
|
|
// Inherit color scheme overrides from parent window. This is to inherit the
|
|
// color scheme of dark themed PBM windows.
|
|
{
|
|
let openerColorSchemeOverride =
|
|
window.opener?.browsingContext?.top.prefersColorSchemeOverride;
|
|
if (
|
|
openerColorSchemeOverride &&
|
|
window.browsingContext == window.browsingContext.top
|
|
) {
|
|
window.browsingContext.prefersColorSchemeOverride = openerColorSchemeOverride;
|
|
}
|
|
}
|
|
|
|
// define a js object to implement nsITreeView
|
|
function pageInfoTreeView(treeid, copycol) {
|
|
// copycol is the index number for the column that we want to add to
|
|
// the copy-n-paste buffer when the user hits accel-c
|
|
this.treeid = treeid;
|
|
this.copycol = copycol;
|
|
this.rows = 0;
|
|
this.tree = null;
|
|
this.data = [];
|
|
this.selection = null;
|
|
this.sortcol = -1;
|
|
this.sortdir = false;
|
|
}
|
|
|
|
pageInfoTreeView.prototype = {
|
|
set rowCount(c) {
|
|
throw new Error("rowCount is a readonly property");
|
|
},
|
|
get rowCount() {
|
|
return this.rows;
|
|
},
|
|
|
|
setTree(tree) {
|
|
this.tree = tree;
|
|
},
|
|
|
|
getCellText(row, column) {
|
|
// row can be null, but js arrays are 0-indexed.
|
|
// colidx cannot be null, but can be larger than the number
|
|
// of columns in the array. In this case it's the fault of
|
|
// whoever typoed while calling this function.
|
|
return this.data[row][column.index] || "";
|
|
},
|
|
|
|
setCellValue(row, column, value) {},
|
|
|
|
setCellText(row, column, value) {
|
|
this.data[row][column.index] = value;
|
|
},
|
|
|
|
addRow(row) {
|
|
this.rows = this.data.push(row);
|
|
this.rowCountChanged(this.rows - 1, 1);
|
|
if (this.selection.count == 0 && this.rowCount && !gImageElement) {
|
|
this.selection.select(0);
|
|
}
|
|
},
|
|
|
|
addRows(rows) {
|
|
for (let row of rows) {
|
|
this.addRow(row);
|
|
}
|
|
},
|
|
|
|
rowCountChanged(index, count) {
|
|
this.tree.rowCountChanged(index, count);
|
|
},
|
|
|
|
invalidate() {
|
|
this.tree.invalidate();
|
|
},
|
|
|
|
clear() {
|
|
if (this.tree) {
|
|
this.tree.rowCountChanged(0, -this.rows);
|
|
}
|
|
this.rows = 0;
|
|
this.data = [];
|
|
},
|
|
|
|
onPageMediaSort(columnname) {
|
|
var tree = document.getElementById(this.treeid);
|
|
var treecol = tree.columns.getNamedColumn(columnname);
|
|
|
|
this.sortdir = gTreeUtils.sort(
|
|
tree,
|
|
this,
|
|
this.data,
|
|
treecol.index,
|
|
function textComparator(a, b) {
|
|
return (a || "").toLowerCase().localeCompare((b || "").toLowerCase());
|
|
},
|
|
this.sortcol,
|
|
this.sortdir
|
|
);
|
|
|
|
for (let col of tree.columns) {
|
|
col.element.removeAttribute("sortActive");
|
|
col.element.removeAttribute("sortDirection");
|
|
}
|
|
treecol.element.setAttribute("sortActive", "true");
|
|
treecol.element.setAttribute(
|
|
"sortDirection",
|
|
this.sortdir ? "ascending" : "descending"
|
|
);
|
|
|
|
this.sortcol = treecol.index;
|
|
},
|
|
|
|
getRowProperties(row) {
|
|
return "";
|
|
},
|
|
getCellProperties(row, column) {
|
|
return "";
|
|
},
|
|
getColumnProperties(column) {
|
|
return "";
|
|
},
|
|
isContainer(index) {
|
|
return false;
|
|
},
|
|
isContainerOpen(index) {
|
|
return false;
|
|
},
|
|
isSeparator(index) {
|
|
return false;
|
|
},
|
|
isSorted() {
|
|
return this.sortcol > -1;
|
|
},
|
|
canDrop(index, orientation) {
|
|
return false;
|
|
},
|
|
drop(row, orientation) {
|
|
return false;
|
|
},
|
|
getParentIndex(index) {
|
|
return 0;
|
|
},
|
|
hasNextSibling(index, after) {
|
|
return false;
|
|
},
|
|
getLevel(index) {
|
|
return 0;
|
|
},
|
|
getImageSrc(row, column) {},
|
|
getCellValue(row, column) {
|
|
let col = column != null ? column : this.copycol;
|
|
return row < 0 || col < 0 ? "" : this.data[row][col] || "";
|
|
},
|
|
toggleOpenState(index) {},
|
|
cycleHeader(col) {},
|
|
selectionChanged() {},
|
|
cycleCell(row, column) {},
|
|
isEditable(row, column) {
|
|
return false;
|
|
},
|
|
};
|
|
|
|
// mmm, yummy. global variables.
|
|
var gDocInfo = null;
|
|
var gImageElement = null;
|
|
|
|
// column number to help using the data array
|
|
const COL_IMAGE_ADDRESS = 0;
|
|
const COL_IMAGE_TYPE = 1;
|
|
const COL_IMAGE_SIZE = 2;
|
|
const COL_IMAGE_ALT = 3;
|
|
const COL_IMAGE_COUNT = 4;
|
|
const COL_IMAGE_NODE = 5;
|
|
const COL_IMAGE_BG = 6;
|
|
|
|
// column number to copy from, second argument to pageInfoTreeView's constructor
|
|
const COPYCOL_NONE = -1;
|
|
const COPYCOL_META_CONTENT = 1;
|
|
const COPYCOL_IMAGE = COL_IMAGE_ADDRESS;
|
|
|
|
// one nsITreeView for each tree in the window
|
|
var gMetaView = new pageInfoTreeView("metatree", COPYCOL_META_CONTENT);
|
|
var gImageView = new pageInfoTreeView("imagetree", COPYCOL_IMAGE);
|
|
|
|
gImageView.getCellProperties = function(row, col) {
|
|
var data = gImageView.data[row];
|
|
var item = gImageView.data[row][COL_IMAGE_NODE];
|
|
var props = "";
|
|
if (
|
|
!checkProtocol(data) ||
|
|
HTMLEmbedElement.isInstance(item) ||
|
|
(HTMLObjectElement.isInstance(item) && !item.type.startsWith("image/"))
|
|
) {
|
|
props += "broken";
|
|
}
|
|
|
|
if (col.element.id == "image-address") {
|
|
props += " ltr";
|
|
}
|
|
|
|
return props;
|
|
};
|
|
|
|
gImageView.onPageMediaSort = function(columnname) {
|
|
var tree = document.getElementById(this.treeid);
|
|
var treecol = tree.columns.getNamedColumn(columnname);
|
|
|
|
var comparator;
|
|
var index = treecol.index;
|
|
if (index == COL_IMAGE_SIZE || index == COL_IMAGE_COUNT) {
|
|
comparator = function numComparator(a, b) {
|
|
return a - b;
|
|
};
|
|
} else {
|
|
comparator = function textComparator(a, b) {
|
|
return (a || "").toLowerCase().localeCompare((b || "").toLowerCase());
|
|
};
|
|
}
|
|
|
|
this.sortdir = gTreeUtils.sort(
|
|
tree,
|
|
this,
|
|
this.data,
|
|
index,
|
|
comparator,
|
|
this.sortcol,
|
|
this.sortdir
|
|
);
|
|
|
|
for (let col of tree.columns) {
|
|
col.element.removeAttribute("sortActive");
|
|
col.element.removeAttribute("sortDirection");
|
|
}
|
|
treecol.element.setAttribute("sortActive", "true");
|
|
treecol.element.setAttribute(
|
|
"sortDirection",
|
|
this.sortdir ? "ascending" : "descending"
|
|
);
|
|
|
|
this.sortcol = index;
|
|
};
|
|
|
|
var gImageHash = {};
|
|
|
|
// localized strings (will be filled in when the document is loaded)
|
|
const MEDIA_STRINGS = {};
|
|
let SIZE_UNKNOWN = "";
|
|
let ALT_NOT_SET = "";
|
|
|
|
// a number of services I'll need later
|
|
// the cache services
|
|
const nsICacheStorageService = Ci.nsICacheStorageService;
|
|
const nsICacheStorage = Ci.nsICacheStorage;
|
|
const cacheService = Cc[
|
|
"@mozilla.org/netwerk/cache-storage-service;1"
|
|
].getService(nsICacheStorageService);
|
|
|
|
var loadContextInfo = Services.loadContextInfo.fromLoadContext(
|
|
window.docShell.QueryInterface(Ci.nsILoadContext),
|
|
false
|
|
);
|
|
var diskStorage = cacheService.diskCacheStorage(loadContextInfo);
|
|
|
|
const nsICookiePermission = Ci.nsICookiePermission;
|
|
|
|
const nsICertificateDialogs = Ci.nsICertificateDialogs;
|
|
const CERTIFICATEDIALOGS_CONTRACTID = "@mozilla.org/nsCertificateDialogs;1";
|
|
|
|
// clipboard helper
|
|
function getClipboardHelper() {
|
|
try {
|
|
return Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
|
|
Ci.nsIClipboardHelper
|
|
);
|
|
} catch (e) {
|
|
// do nothing, later code will handle the error
|
|
return null;
|
|
}
|
|
}
|
|
const gClipboardHelper = getClipboardHelper();
|
|
|
|
/* Called when PageInfo window is loaded. Arguments are:
|
|
* window.arguments[0] - (optional) an object consisting of
|
|
* - doc: (optional) document to use for source. if not provided,
|
|
* the calling window's document will be used
|
|
* - initialTab: (optional) id of the inital tab to display
|
|
*/
|
|
async function onLoadPageInfo() {
|
|
[
|
|
SIZE_UNKNOWN,
|
|
ALT_NOT_SET,
|
|
MEDIA_STRINGS.img,
|
|
MEDIA_STRINGS["bg-img"],
|
|
MEDIA_STRINGS["border-img"],
|
|
MEDIA_STRINGS["list-img"],
|
|
MEDIA_STRINGS.cursor,
|
|
MEDIA_STRINGS.object,
|
|
MEDIA_STRINGS.embed,
|
|
MEDIA_STRINGS.link,
|
|
MEDIA_STRINGS.input,
|
|
MEDIA_STRINGS.video,
|
|
MEDIA_STRINGS.audio,
|
|
] = await document.l10n.formatValues([
|
|
"image-size-unknown",
|
|
"not-set-alternative-text",
|
|
"media-img",
|
|
"media-bg-img",
|
|
"media-border-img",
|
|
"media-list-img",
|
|
"media-cursor",
|
|
"media-object",
|
|
"media-embed",
|
|
"media-link",
|
|
"media-input",
|
|
"media-video",
|
|
"media-audio",
|
|
]);
|
|
|
|
const args =
|
|
"arguments" in window &&
|
|
window.arguments.length >= 1 &&
|
|
window.arguments[0];
|
|
|
|
// Init media view
|
|
let imageTree = document.getElementById("imagetree");
|
|
imageTree.view = gImageView;
|
|
|
|
imageTree.controllers.appendController(treeController);
|
|
|
|
document
|
|
.getElementById("metatree")
|
|
.controllers.appendController(treeController);
|
|
|
|
// Select the requested tab, if the name is specified
|
|
await loadTab(args);
|
|
|
|
// Emit init event for tests
|
|
window.dispatchEvent(new Event("page-info-init"));
|
|
}
|
|
|
|
async function loadPageInfo(browsingContext, imageElement, browser) {
|
|
browser = browser || window.opener.gBrowser.selectedBrowser;
|
|
browsingContext = browsingContext || browser.browsingContext;
|
|
|
|
let actor = browsingContext.currentWindowGlobal.getActor("PageInfo");
|
|
|
|
let result = await actor.sendQuery("PageInfo:getData");
|
|
await onNonMediaPageInfoLoad(browser, result, imageElement);
|
|
|
|
// Here, we are walking the frame tree via BrowsingContexts to collect all of the
|
|
// media information for each frame
|
|
let contextsToVisit = [browsingContext];
|
|
while (contextsToVisit.length) {
|
|
let currContext = contextsToVisit.pop();
|
|
let global = currContext.currentWindowGlobal;
|
|
|
|
if (!global) {
|
|
continue;
|
|
}
|
|
|
|
let subframeActor = global.getActor("PageInfo");
|
|
let mediaResult = await subframeActor.sendQuery("PageInfo:getMediaData");
|
|
for (let item of mediaResult.mediaItems) {
|
|
addImage(item);
|
|
}
|
|
selectImage();
|
|
contextsToVisit.push(...currContext.children);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* onNonMediaPageInfoLoad is responsible for populating the page info
|
|
* UI other than the media tab. This includes general, permissions, and security.
|
|
*/
|
|
async function onNonMediaPageInfoLoad(browser, pageInfoData, imageInfo) {
|
|
const { docInfo, windowInfo } = pageInfoData;
|
|
let uri = Services.io.newURI(docInfo.documentURIObject.spec);
|
|
let principal = docInfo.principal;
|
|
gDocInfo = docInfo;
|
|
|
|
gImageElement = imageInfo;
|
|
var titleFormat = windowInfo.isTopWindow
|
|
? "page-info-page"
|
|
: "page-info-frame";
|
|
document.l10n.setAttributes(document.documentElement, titleFormat, {
|
|
website: docInfo.location,
|
|
});
|
|
|
|
document
|
|
.getElementById("main-window")
|
|
.setAttribute("relatedUrl", docInfo.location);
|
|
|
|
await makeGeneralTab(pageInfoData.metaViewRows, docInfo);
|
|
if (
|
|
uri.spec.startsWith("about:neterror") ||
|
|
uri.spec.startsWith("about:certerror") ||
|
|
uri.spec.startsWith("about:httpsonlyerror")
|
|
) {
|
|
uri = browser.currentURI;
|
|
principal = Services.scriptSecurityManager.createContentPrincipal(
|
|
uri,
|
|
browser.contentPrincipal.originAttributes
|
|
);
|
|
}
|
|
onLoadPermission(uri, principal);
|
|
securityOnLoad(uri, windowInfo);
|
|
}
|
|
|
|
function resetPageInfo(args) {
|
|
/* Reset Meta tags part */
|
|
gMetaView.clear();
|
|
|
|
/* Reset Media tab */
|
|
var mediaTab = document.getElementById("mediaTab");
|
|
if (!mediaTab.hidden) {
|
|
mediaTab.hidden = true;
|
|
}
|
|
gImageView.clear();
|
|
gImageHash = {};
|
|
|
|
/* Rebuild the data */
|
|
loadTab(args);
|
|
}
|
|
|
|
function doHelpButton() {
|
|
const helpTopics = {
|
|
generalPanel: "pageinfo_general",
|
|
mediaPanel: "pageinfo_media",
|
|
permPanel: "pageinfo_permissions",
|
|
securityPanel: "pageinfo_security",
|
|
};
|
|
|
|
var deck = document.getElementById("mainDeck");
|
|
var helpdoc = helpTopics[deck.selectedPanel.id] || "pageinfo_general";
|
|
openHelpLink(helpdoc);
|
|
}
|
|
|
|
function showTab(id) {
|
|
var deck = document.getElementById("mainDeck");
|
|
var pagel = document.getElementById(id + "Panel");
|
|
deck.selectedPanel = pagel;
|
|
}
|
|
|
|
async function loadTab(args) {
|
|
// If the "View Image Info" context menu item was used, the related image
|
|
// element is provided as an argument. This can't be a background image.
|
|
let imageElement = args?.imageElement;
|
|
let browsingContext = args?.browsingContext;
|
|
let browser = args?.browser;
|
|
|
|
/* Load the page info */
|
|
await loadPageInfo(browsingContext, imageElement, browser);
|
|
|
|
var initialTab = args?.initialTab || "generalTab";
|
|
var radioGroup = document.getElementById("viewGroup");
|
|
initialTab =
|
|
document.getElementById(initialTab) ||
|
|
document.getElementById("generalTab");
|
|
radioGroup.selectedItem = initialTab;
|
|
radioGroup.selectedItem.doCommand();
|
|
radioGroup.focus({ focusVisible: false });
|
|
}
|
|
|
|
function openCacheEntry(key, cb) {
|
|
var checkCacheListener = {
|
|
onCacheEntryCheck(entry) {
|
|
return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
|
|
},
|
|
onCacheEntryAvailable(entry, isNew, status) {
|
|
cb(entry);
|
|
},
|
|
};
|
|
diskStorage.asyncOpenURI(
|
|
Services.io.newURI(key),
|
|
"",
|
|
nsICacheStorage.OPEN_READONLY,
|
|
checkCacheListener
|
|
);
|
|
}
|
|
|
|
async function makeGeneralTab(metaViewRows, docInfo) {
|
|
// Sets Title in the General Tab, set to "Untitled Page" if no title found
|
|
if (docInfo.title) {
|
|
document.getElementById("titletext").value = docInfo.title;
|
|
} else {
|
|
document.l10n.setAttributes(
|
|
document.getElementById("titletext"),
|
|
"no-page-title"
|
|
);
|
|
}
|
|
|
|
var url = docInfo.location;
|
|
setItemValue("urltext", url);
|
|
|
|
var referrer = "referrer" in docInfo && docInfo.referrer;
|
|
setItemValue("refertext", referrer);
|
|
|
|
var mode =
|
|
"compatMode" in docInfo && docInfo.compatMode == "BackCompat"
|
|
? "general-quirks-mode"
|
|
: "general-strict-mode";
|
|
document.l10n.setAttributes(document.getElementById("modetext"), mode);
|
|
|
|
// find out the mime type
|
|
setItemValue("typetext", docInfo.contentType);
|
|
|
|
// get the document characterset
|
|
var encoding = docInfo.characterSet;
|
|
document.getElementById("encodingtext").value = encoding;
|
|
|
|
let length = metaViewRows.length;
|
|
|
|
var metaGroup = document.getElementById("metaTags");
|
|
if (!length) {
|
|
metaGroup.style.visibility = "hidden";
|
|
} else {
|
|
document.l10n.setAttributes(
|
|
document.getElementById("metaTagsCaption"),
|
|
"general-meta-tags",
|
|
{ tags: length }
|
|
);
|
|
|
|
document.getElementById("metatree").view = gMetaView;
|
|
|
|
// Add the metaViewRows onto the general tab's meta info tree.
|
|
gMetaView.addRows(metaViewRows);
|
|
|
|
metaGroup.style.removeProperty("visibility");
|
|
}
|
|
|
|
var modifiedText = formatDate(
|
|
docInfo.lastModified,
|
|
await document.l10n.formatValue("not-set-date")
|
|
);
|
|
document.getElementById("modifiedtext").value = modifiedText;
|
|
|
|
// get cache info
|
|
var cacheKey = url.replace(/#.*$/, "");
|
|
openCacheEntry(cacheKey, function(cacheEntry) {
|
|
if (cacheEntry) {
|
|
var pageSize = cacheEntry.dataSize;
|
|
var kbSize = formatNumber(Math.round((pageSize / 1024) * 100) / 100);
|
|
document.l10n.setAttributes(
|
|
document.getElementById("sizetext"),
|
|
"properties-general-size",
|
|
{ kb: kbSize, bytes: formatNumber(pageSize) }
|
|
);
|
|
} else {
|
|
setItemValue("sizetext", null);
|
|
}
|
|
});
|
|
}
|
|
|
|
async function addImage({ url, type, alt, altNotProvided, element, isBg }) {
|
|
if (!url) {
|
|
return;
|
|
}
|
|
|
|
if (altNotProvided) {
|
|
alt = ALT_NOT_SET;
|
|
}
|
|
|
|
if (!gImageHash.hasOwnProperty(url)) {
|
|
gImageHash[url] = {};
|
|
}
|
|
if (!gImageHash[url].hasOwnProperty(type)) {
|
|
gImageHash[url][type] = {};
|
|
}
|
|
if (!gImageHash[url][type].hasOwnProperty(alt)) {
|
|
gImageHash[url][type][alt] = gImageView.data.length;
|
|
var row = [url, MEDIA_STRINGS[type], SIZE_UNKNOWN, alt, 1, element, isBg];
|
|
gImageView.addRow(row);
|
|
|
|
// Fill in cache data asynchronously
|
|
openCacheEntry(url, function(cacheEntry) {
|
|
// The data at row[2] corresponds to the data size.
|
|
if (cacheEntry) {
|
|
let value = cacheEntry.dataSize;
|
|
// If value is not -1 then replace with actual value, else keep as "unknown"
|
|
if (value != -1) {
|
|
let kbSize = Number(Math.round((value / 1024) * 100) / 100);
|
|
document.l10n
|
|
.formatValue("media-file-size", { size: kbSize })
|
|
.then(function(response) {
|
|
row[2] = response;
|
|
// Invalidate the row to trigger a repaint.
|
|
gImageView.tree.invalidateRow(gImageView.data.indexOf(row));
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
if (gImageView.data.length == 1) {
|
|
document.getElementById("mediaTab").hidden = false;
|
|
}
|
|
} else {
|
|
var i = gImageHash[url][type][alt];
|
|
gImageView.data[i][COL_IMAGE_COUNT]++;
|
|
// The same image can occur several times on the page at different sizes.
|
|
// If the "View Image Info" context menu item was used, ensure we select
|
|
// the correct element.
|
|
if (
|
|
!gImageView.data[i][COL_IMAGE_BG] &&
|
|
gImageElement &&
|
|
url == gImageElement.currentSrc &&
|
|
gImageElement.width == element.width &&
|
|
gImageElement.height == element.height &&
|
|
gImageElement.imageText == element.imageText
|
|
) {
|
|
gImageView.data[i][COL_IMAGE_NODE] = element;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Link Stuff
|
|
function onBeginLinkDrag(event, urlField, descField) {
|
|
if (event.originalTarget.localName != "treechildren") {
|
|
return;
|
|
}
|
|
|
|
var tree = event.target;
|
|
if (tree.localName != "tree") {
|
|
tree = tree.parentNode;
|
|
}
|
|
|
|
var row = tree.getRowAt(event.clientX, event.clientY);
|
|
if (row == -1) {
|
|
return;
|
|
}
|
|
|
|
// Adding URL flavor
|
|
var col = tree.columns[urlField];
|
|
var url = tree.view.getCellText(row, col);
|
|
col = tree.columns[descField];
|
|
var desc = tree.view.getCellText(row, col);
|
|
|
|
var dt = event.dataTransfer;
|
|
dt.setData("text/x-moz-url", url + "\n" + desc);
|
|
dt.setData("text/url-list", url);
|
|
dt.setData("text/plain", url);
|
|
}
|
|
|
|
// Image Stuff
|
|
function getSelectedRows(tree) {
|
|
var start = {};
|
|
var end = {};
|
|
var numRanges = tree.view.selection.getRangeCount();
|
|
|
|
var rowArray = [];
|
|
for (var t = 0; t < numRanges; t++) {
|
|
tree.view.selection.getRangeAt(t, start, end);
|
|
for (var v = start.value; v <= end.value; v++) {
|
|
rowArray.push(v);
|
|
}
|
|
}
|
|
|
|
return rowArray;
|
|
}
|
|
|
|
function getSelectedRow(tree) {
|
|
var rows = getSelectedRows(tree);
|
|
return rows.length == 1 ? rows[0] : -1;
|
|
}
|
|
|
|
async function selectSaveFolder(aCallback) {
|
|
const { nsIFile, nsIFilePicker } = Ci;
|
|
let titleText = await document.l10n.formatValue("media-select-folder");
|
|
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
|
|
let fpCallback = function fpCallback_done(aResult) {
|
|
if (aResult == nsIFilePicker.returnOK) {
|
|
aCallback(fp.file.QueryInterface(nsIFile));
|
|
} else {
|
|
aCallback(null);
|
|
}
|
|
};
|
|
|
|
fp.init(window, titleText, nsIFilePicker.modeGetFolder);
|
|
fp.appendFilters(nsIFilePicker.filterAll);
|
|
try {
|
|
let initialDir = Services.prefs.getComplexValue(
|
|
"browser.download.dir",
|
|
nsIFile
|
|
);
|
|
if (initialDir) {
|
|
fp.displayDirectory = initialDir;
|
|
}
|
|
} catch (ex) {}
|
|
fp.open(fpCallback);
|
|
}
|
|
|
|
function saveMedia() {
|
|
var tree = document.getElementById("imagetree");
|
|
var rowArray = getSelectedRows(tree);
|
|
let ReferrerInfo = Components.Constructor(
|
|
"@mozilla.org/referrer-info;1",
|
|
"nsIReferrerInfo",
|
|
"init"
|
|
);
|
|
|
|
if (rowArray.length == 1) {
|
|
let row = rowArray[0];
|
|
let item = gImageView.data[row][COL_IMAGE_NODE];
|
|
let url = gImageView.data[row][COL_IMAGE_ADDRESS];
|
|
|
|
if (url) {
|
|
var titleKey = "SaveImageTitle";
|
|
|
|
if (HTMLVideoElement.isInstance(item)) {
|
|
titleKey = "SaveVideoTitle";
|
|
} else if (HTMLAudioElement.isInstance(item)) {
|
|
titleKey = "SaveAudioTitle";
|
|
}
|
|
|
|
// Bug 1565216 to evaluate passing referrer as item.baseURL
|
|
let referrerInfo = new ReferrerInfo(
|
|
Ci.nsIReferrerInfo.EMPTY,
|
|
true,
|
|
Services.io.newURI(item.baseURI)
|
|
);
|
|
let cookieJarSettings = E10SUtils.deserializeCookieJarSettings(
|
|
gDocInfo.cookieJarSettings
|
|
);
|
|
saveURL(
|
|
url,
|
|
null,
|
|
null,
|
|
titleKey,
|
|
false,
|
|
false,
|
|
referrerInfo,
|
|
cookieJarSettings,
|
|
null,
|
|
gDocInfo.isContentWindowPrivate,
|
|
gDocInfo.principal
|
|
);
|
|
}
|
|
} else {
|
|
selectSaveFolder(function(aDirectory) {
|
|
if (aDirectory) {
|
|
var saveAnImage = function(aURIString, aChosenData, aBaseURI) {
|
|
uniqueFile(aChosenData.file);
|
|
|
|
let referrerInfo = new ReferrerInfo(
|
|
Ci.nsIReferrerInfo.EMPTY,
|
|
true,
|
|
aBaseURI
|
|
);
|
|
let cookieJarSettings = E10SUtils.deserializeCookieJarSettings(
|
|
gDocInfo.cookieJarSettings
|
|
);
|
|
internalSave(
|
|
aURIString,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
false,
|
|
"SaveImageTitle",
|
|
aChosenData,
|
|
referrerInfo,
|
|
cookieJarSettings,
|
|
null,
|
|
false,
|
|
null,
|
|
gDocInfo.isContentWindowPrivate,
|
|
gDocInfo.principal
|
|
);
|
|
};
|
|
|
|
for (var i = 0; i < rowArray.length; i++) {
|
|
let v = rowArray[i];
|
|
let dir = aDirectory.clone();
|
|
let item = gImageView.data[v][COL_IMAGE_NODE];
|
|
let uriString = gImageView.data[v][COL_IMAGE_ADDRESS];
|
|
let uri = Services.io.newURI(uriString);
|
|
|
|
try {
|
|
uri.QueryInterface(Ci.nsIURL);
|
|
dir.append(decodeURIComponent(uri.fileName));
|
|
} catch (ex) {
|
|
// data:/blob: uris
|
|
// Supply a dummy filename, otherwise Download Manager
|
|
// will try to delete the base directory on failure.
|
|
dir.append(gImageView.data[v][COL_IMAGE_TYPE]);
|
|
}
|
|
|
|
if (i == 0) {
|
|
saveAnImage(
|
|
uriString,
|
|
new AutoChosen(dir, uri),
|
|
Services.io.newURI(item.baseURI)
|
|
);
|
|
} else {
|
|
// This delay is a hack which prevents the download manager
|
|
// from opening many times. See bug 377339.
|
|
setTimeout(
|
|
saveAnImage,
|
|
200,
|
|
uriString,
|
|
new AutoChosen(dir, uri),
|
|
Services.io.newURI(item.baseURI)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function onImageSelect() {
|
|
var previewBox = document.getElementById("mediaPreviewBox");
|
|
var mediaSaveBox = document.getElementById("mediaSaveBox");
|
|
var splitter = document.getElementById("mediaSplitter");
|
|
var tree = document.getElementById("imagetree");
|
|
var count = tree.view.selection.count;
|
|
if (count == 0) {
|
|
previewBox.collapsed = true;
|
|
mediaSaveBox.collapsed = true;
|
|
splitter.collapsed = true;
|
|
tree.setAttribute("flex", "1");
|
|
} else if (count > 1) {
|
|
splitter.collapsed = true;
|
|
previewBox.collapsed = true;
|
|
mediaSaveBox.collapsed = false;
|
|
tree.setAttribute("flex", "1");
|
|
} else {
|
|
mediaSaveBox.collapsed = true;
|
|
splitter.collapsed = false;
|
|
previewBox.collapsed = false;
|
|
tree.setAttribute("flex", "0");
|
|
makePreview(getSelectedRows(tree)[0]);
|
|
}
|
|
}
|
|
|
|
// Makes the media preview (image, video, etc) for the selected row on the media tab.
|
|
function makePreview(row) {
|
|
var item = gImageView.data[row][COL_IMAGE_NODE];
|
|
var url = gImageView.data[row][COL_IMAGE_ADDRESS];
|
|
var isBG = gImageView.data[row][COL_IMAGE_BG];
|
|
var isAudio = false;
|
|
|
|
setItemValue("imageurltext", url);
|
|
setItemValue("imagetext", item.imageText);
|
|
setItemValue("imagelongdesctext", item.longDesc);
|
|
|
|
// get cache info
|
|
var cacheKey = url.replace(/#.*$/, "");
|
|
openCacheEntry(cacheKey, function(cacheEntry) {
|
|
// find out the file size
|
|
if (cacheEntry) {
|
|
let imageSize = cacheEntry.dataSize;
|
|
var kbSize = Math.round((imageSize / 1024) * 100) / 100;
|
|
document.l10n.setAttributes(
|
|
document.getElementById("imagesizetext"),
|
|
"properties-general-size",
|
|
{ kb: formatNumber(kbSize), bytes: formatNumber(imageSize) }
|
|
);
|
|
} else {
|
|
document.l10n.setAttributes(
|
|
document.getElementById("imagesizetext"),
|
|
"media-unknown-not-cached"
|
|
);
|
|
}
|
|
|
|
var mimeType = item.mimeType || this.getContentTypeFromHeaders(cacheEntry);
|
|
var numFrames = item.numFrames;
|
|
|
|
let element = document.getElementById("imagetypetext");
|
|
var imageType;
|
|
if (mimeType) {
|
|
// We found the type, try to display it nicely
|
|
let imageMimeType = /^image\/(.*)/i.exec(mimeType);
|
|
if (imageMimeType) {
|
|
imageType = imageMimeType[1].toUpperCase();
|
|
if (numFrames > 1) {
|
|
document.l10n.setAttributes(element, "media-animated-image-type", {
|
|
type: imageType,
|
|
frames: numFrames,
|
|
});
|
|
} else {
|
|
document.l10n.setAttributes(element, "media-image-type", {
|
|
type: imageType,
|
|
});
|
|
}
|
|
} else {
|
|
// the MIME type doesn't begin with image/, display the raw type
|
|
element.setAttribute("value", mimeType);
|
|
element.removeAttribute("data-l10n-id");
|
|
}
|
|
} else {
|
|
// We couldn't find the type, fall back to the value in the treeview
|
|
element.setAttribute("value", gImageView.data[row][COL_IMAGE_TYPE]);
|
|
element.removeAttribute("data-l10n-id");
|
|
}
|
|
|
|
var imageContainer = document.getElementById("theimagecontainer");
|
|
var oldImage = document.getElementById("thepreviewimage");
|
|
|
|
var isProtocolAllowed = checkProtocol(gImageView.data[row]);
|
|
|
|
var newImage = new Image();
|
|
newImage.id = "thepreviewimage";
|
|
var physWidth = 0,
|
|
physHeight = 0;
|
|
var width = 0,
|
|
height = 0;
|
|
|
|
let triggeringPrinStr = E10SUtils.serializePrincipal(gDocInfo.principal);
|
|
if (
|
|
(item.HTMLLinkElement ||
|
|
item.HTMLInputElement ||
|
|
item.HTMLImageElement ||
|
|
item.SVGImageElement ||
|
|
(item.HTMLObjectElement && mimeType && mimeType.startsWith("image/")) ||
|
|
isBG) &&
|
|
isProtocolAllowed
|
|
) {
|
|
function loadOrErrorListener() {
|
|
newImage.removeEventListener("load", loadOrErrorListener);
|
|
newImage.removeEventListener("error", loadOrErrorListener);
|
|
physWidth = newImage.width || 0;
|
|
physHeight = newImage.height || 0;
|
|
|
|
// "width" and "height" attributes must be set to newImage,
|
|
// even if there is no "width" or "height attribute in item;
|
|
// otherwise, the preview image cannot be displayed correctly.
|
|
// Since the image might have been loaded out-of-process, we expect
|
|
// the item to tell us its width / height dimensions. Failing that
|
|
// the item should tell us the natural dimensions of the image. Finally
|
|
// failing that, we'll assume that the image was never loaded in the
|
|
// other process (this can be true for favicons, for example), and so
|
|
// we'll assume that we can use the natural dimensions of the newImage
|
|
// we just created. If the natural dimensions of newImage are not known
|
|
// then the image is probably broken.
|
|
if (!isBG) {
|
|
newImage.width =
|
|
("width" in item && item.width) || newImage.naturalWidth;
|
|
newImage.height =
|
|
("height" in item && item.height) || newImage.naturalHeight;
|
|
} else {
|
|
// the Width and Height of an HTML tag should not be used for its background image
|
|
// (for example, "table" can have "width" or "height" attributes)
|
|
newImage.width = item.naturalWidth || newImage.naturalWidth;
|
|
newImage.height = item.naturalHeight || newImage.naturalHeight;
|
|
}
|
|
|
|
if (item.SVGImageElement) {
|
|
newImage.width = item.SVGImageElementWidth;
|
|
newImage.height = item.SVGImageElementHeight;
|
|
}
|
|
|
|
width = newImage.width;
|
|
height = newImage.height;
|
|
|
|
document.getElementById("theimagecontainer").collapsed = false;
|
|
document.getElementById("brokenimagecontainer").collapsed = true;
|
|
|
|
if (url) {
|
|
if (width != physWidth || height != physHeight) {
|
|
document.l10n.setAttributes(
|
|
document.getElementById("imagedimensiontext"),
|
|
"media-dimensions-scaled",
|
|
{
|
|
dimx: formatNumber(physWidth),
|
|
dimy: formatNumber(physHeight),
|
|
scaledx: formatNumber(width),
|
|
scaledy: formatNumber(height),
|
|
}
|
|
);
|
|
} else {
|
|
document.l10n.setAttributes(
|
|
document.getElementById("imagedimensiontext"),
|
|
"media-dimensions",
|
|
{ dimx: formatNumber(width), dimy: formatNumber(height) }
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// We need to wait for the image to finish loading before using width & height
|
|
newImage.addEventListener("load", loadOrErrorListener);
|
|
newImage.addEventListener("error", loadOrErrorListener);
|
|
|
|
newImage.setAttribute("triggeringprincipal", triggeringPrinStr);
|
|
newImage.setAttribute("src", url);
|
|
} else {
|
|
// Handle the case where newImage is not used for width & height
|
|
if (item.HTMLVideoElement && isProtocolAllowed) {
|
|
newImage = document.createElement("video");
|
|
newImage.id = "thepreviewimage";
|
|
newImage.setAttribute("triggeringprincipal", triggeringPrinStr);
|
|
newImage.src = url;
|
|
newImage.controls = true;
|
|
width = physWidth = item.videoWidth;
|
|
height = physHeight = item.videoHeight;
|
|
|
|
document.getElementById("theimagecontainer").collapsed = false;
|
|
document.getElementById("brokenimagecontainer").collapsed = true;
|
|
} else if (item.HTMLAudioElement && isProtocolAllowed) {
|
|
newImage = new Audio();
|
|
newImage.id = "thepreviewimage";
|
|
newImage.setAttribute("triggeringprincipal", triggeringPrinStr);
|
|
newImage.src = url;
|
|
newImage.controls = true;
|
|
isAudio = true;
|
|
|
|
document.getElementById("theimagecontainer").collapsed = false;
|
|
document.getElementById("brokenimagecontainer").collapsed = true;
|
|
} else {
|
|
// fallback image for protocols not allowed (e.g., javascript:)
|
|
// or elements not [yet] handled (e.g., object, embed).
|
|
document.getElementById("brokenimagecontainer").collapsed = false;
|
|
document.getElementById("theimagecontainer").collapsed = true;
|
|
}
|
|
|
|
if (url && !isAudio) {
|
|
document.l10n.setAttributes(
|
|
document.getElementById("imagedimensiontext"),
|
|
"media-dimensions",
|
|
{ dimx: formatNumber(width), dimy: formatNumber(height) }
|
|
);
|
|
}
|
|
}
|
|
|
|
imageContainer.removeChild(oldImage);
|
|
imageContainer.appendChild(newImage);
|
|
});
|
|
}
|
|
|
|
function getContentTypeFromHeaders(cacheEntryDescriptor) {
|
|
if (!cacheEntryDescriptor) {
|
|
return null;
|
|
}
|
|
|
|
let headers = cacheEntryDescriptor.getMetaDataElement("response-head");
|
|
let type = /^Content-Type:\s*(.*?)\s*(?:\;|$)/im.exec(headers);
|
|
return type && type[1];
|
|
}
|
|
|
|
function setItemValue(id, value) {
|
|
var item = document.getElementById(id);
|
|
item.closest("tr").hidden = !value;
|
|
if (value) {
|
|
item.value = value;
|
|
}
|
|
}
|
|
|
|
function formatNumber(number) {
|
|
return (+number).toLocaleString(); // coerce number to a numeric value before calling toLocaleString()
|
|
}
|
|
|
|
function formatDate(datestr, unknown) {
|
|
var date = new Date(datestr);
|
|
if (!date.valueOf()) {
|
|
return unknown;
|
|
}
|
|
|
|
const dateTimeFormatter = new Services.intl.DateTimeFormat(undefined, {
|
|
dateStyle: "long",
|
|
timeStyle: "long",
|
|
});
|
|
return dateTimeFormatter.format(date);
|
|
}
|
|
|
|
let treeController = {
|
|
supportsCommand(command) {
|
|
return command == "cmd_copy" || command == "cmd_selectAll";
|
|
},
|
|
|
|
isCommandEnabled(command) {
|
|
return true; // not worth checking for this
|
|
},
|
|
|
|
doCommand(command) {
|
|
switch (command) {
|
|
case "cmd_copy":
|
|
doCopy();
|
|
break;
|
|
case "cmd_selectAll":
|
|
document.activeElement.view.selection.selectAll();
|
|
break;
|
|
}
|
|
},
|
|
};
|
|
|
|
function doCopy() {
|
|
if (!gClipboardHelper) {
|
|
return;
|
|
}
|
|
|
|
var elem = document.commandDispatcher.focusedElement;
|
|
|
|
if (elem && elem.localName == "tree") {
|
|
var view = elem.view;
|
|
var selection = view.selection;
|
|
var text = [],
|
|
tmp = "";
|
|
var min = {},
|
|
max = {};
|
|
|
|
var count = selection.getRangeCount();
|
|
|
|
for (var i = 0; i < count; i++) {
|
|
selection.getRangeAt(i, min, max);
|
|
|
|
for (var row = min.value; row <= max.value; row++) {
|
|
tmp = view.getCellValue(row, null);
|
|
if (tmp) {
|
|
text.push(tmp);
|
|
}
|
|
}
|
|
}
|
|
gClipboardHelper.copyString(text.join("\n"));
|
|
}
|
|
}
|
|
|
|
function doSelectAllMedia() {
|
|
var tree = document.getElementById("imagetree");
|
|
|
|
if (tree) {
|
|
tree.view.selection.selectAll();
|
|
}
|
|
}
|
|
|
|
function selectImage() {
|
|
if (!gImageElement) {
|
|
return;
|
|
}
|
|
|
|
var tree = document.getElementById("imagetree");
|
|
for (var i = 0; i < tree.view.rowCount; i++) {
|
|
// If the image row element is the image selected from the "View Image Info" context menu item.
|
|
let image = gImageView.data[i][COL_IMAGE_NODE];
|
|
if (
|
|
!gImageView.data[i][COL_IMAGE_BG] &&
|
|
gImageElement.currentSrc == gImageView.data[i][COL_IMAGE_ADDRESS] &&
|
|
gImageElement.width == image.width &&
|
|
gImageElement.height == image.height &&
|
|
gImageElement.imageText == image.imageText
|
|
) {
|
|
tree.view.selection.select(i);
|
|
tree.ensureRowIsVisible(i);
|
|
tree.focus();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
function checkProtocol(img) {
|
|
var url = img[COL_IMAGE_ADDRESS];
|
|
return (
|
|
/^data:image\//i.test(url) ||
|
|
/^(https?|file|about|chrome|resource):/.test(url)
|
|
);
|
|
}
|