diff options
Diffstat (limited to 'browser/base/content/pageinfo')
-rw-r--r-- | browser/base/content/pageinfo/pageInfo.css | 89 | ||||
-rw-r--r-- | browser/base/content/pageinfo/pageInfo.js | 1172 | ||||
-rw-r--r-- | browser/base/content/pageinfo/pageInfo.xhtml | 411 | ||||
-rw-r--r-- | browser/base/content/pageinfo/permissions.js | 240 | ||||
-rw-r--r-- | browser/base/content/pageinfo/security.js | 426 |
5 files changed, 2338 insertions, 0 deletions
diff --git a/browser/base/content/pageinfo/pageInfo.css b/browser/base/content/pageinfo/pageInfo.css new file mode 100644 index 0000000000..d3c023a4c0 --- /dev/null +++ b/browser/base/content/pageinfo/pageInfo.css @@ -0,0 +1,89 @@ +/* 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/. */ + +:root { + min-width: 24em; + min-height: 24em; +} + +#mainDeck { + padding: 10px; + min-height: 0; +} + +#mediaPreviewBox, +#imagecontainerbox, +#thepreviewimage, +#mainDeck > vbox { + min-width: 0; + min-height: 0; +} + +#viewGroup > radio > .radio-label-box { + flex-direction: column; + align-items: center; +} + +/* Hide the radio button for the section headers */ +#viewGroup > radio > .radio-check { + display: none; +} + +#thepreviewimage { + margin: 1em auto; + flex: none; + display: block; +} + +table { + border-spacing: 0; +} + +.tableSeparator { + height: 6px; +} + +th, td { + padding: 0; +} + +th { + font: inherit; + text-align: start; + padding-inline-end: .5em; +} + +/* + Make the first column shrink to its min-content, except for #securityTable + which has full sentences in its first column. +*/ +table:not(#securityTable) th { + width: 0; +} + +th > label, +td > input, +.table-split-column { + width: 100%; + margin-block: 1px 4px; +} + +.table-split-column { + display: flex; + align-items: center; +} + +.table-split-column > label, +.table-split-column > input { + flex: 1 auto; +} + +.table-split-column > button { + flex-shrink: 0; +} + +#hostText { + flex: 1; + margin-top: 1px; /* same margin as adjacent label */ +} diff --git a/browser/base/content/pageinfo/pageInfo.js b/browser/base/content/pageinfo/pageInfo.js new file mode 100644 index 0000000000..e3339afdda --- /dev/null +++ b/browser/base/content/pageinfo/pageInfo.js @@ -0,0 +1,1172 @@ +/* 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) + ); +} diff --git a/browser/base/content/pageinfo/pageInfo.xhtml b/browser/base/content/pageinfo/pageInfo.xhtml new file mode 100644 index 0000000000..5e4c893b22 --- /dev/null +++ b/browser/base/content/pageinfo/pageInfo.xhtml @@ -0,0 +1,411 @@ +<?xml version="1.0"?> +# 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/. + +<?xml-stylesheet href="chrome://browser/content/pageinfo/pageInfo.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/pageInfo.css" type="text/css"?> + +<window id="main-window" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + data-l10n-id="page-info-window" + data-l10n-attrs="style" + windowtype="Browser:page-info" + onload="onLoadPageInfo()" + align="stretch" + screenX="10" screenY="10" + persist="screenX screenY width height sizemode"> + + <linkset> + <html:link rel="localization" href="browser/pageInfo.ftl"/> + </linkset> + #ifdef XP_MACOSX + #include ../macWindow.inc.xhtml + #else + <script src="chrome://global/content/globalOverlay.js"/> + <script src="chrome://global/content/editMenuOverlay.js"/> + <script src="chrome://browser/content/utilityOverlay.js"/> + #endif + <script src="chrome://global/content/contentAreaUtils.js"/> + <script src="chrome://global/content/treeUtils.js"/> + <script src="chrome://browser/content/pageinfo/pageInfo.js"/> + <script src="chrome://browser/content/pageinfo/permissions.js"/> + <script src="chrome://browser/content/pageinfo/security.js"/> + + <stringbundleset id="pageinfobundleset"> + <stringbundle id="pkiBundle" src="chrome://pippki/locale/pippki.properties"/> + <stringbundle id="browserBundle" src="chrome://browser/locale/browser.properties"/> + </stringbundleset> + + <commandset id="pageInfoCommandSet"> + <command id="cmd_close" oncommand="window.close();"/> + <command id="cmd_help" oncommand="doHelpButton();"/> + </commandset> + + <keyset id="pageInfoKeySet"> + <key data-l10n-id="close-dialog" data-l10n-attrs="key" modifiers="accel" command="cmd_close"/> + <key keycode="VK_ESCAPE" command="cmd_close"/> +#ifdef XP_MACOSX + <key key="." modifiers="meta" command="cmd_close"/> +#else + <key keycode="VK_F1" command="cmd_help"/> +#endif + <key data-l10n-id="copy" data-l10n-attrs="key" modifiers="accel" command="cmd_copy"/> + <key data-l10n-id="select-all" data-l10n-attrs="key" modifiers="accel" command="cmd_selectAll"/> + <key data-l10n-id="select-all" data-l10n-attrs="key" modifiers="alt" command="cmd_selectAll"/> + </keyset> + + <menupopup id="picontext"> + <menuitem id="menu_selectall" data-l10n-id="menu-select-all" command="cmd_selectAll"/> + <menuitem id="menu_copy" data-l10n-id="menu-copy" command="cmd_copy"/> + </menupopup> + + <vbox id="topBar"> + <radiogroup id="viewGroup" class="chromeclass-toolbar" orient="horizontal"> + <radio id="generalTab" data-l10n-id="general-tab" + oncommand="showTab('general');"/> + <radio id="mediaTab" data-l10n-id="media-tab" + oncommand="showTab('media');" hidden="true"/> + <radio id="permTab" data-l10n-id="perm-tab" + oncommand="showTab('perm');"/> + <radio id="securityTab" data-l10n-id="security-tab" + oncommand="showTab('security');"/> + </radiogroup> + </vbox> + + <deck id="mainDeck" flex="1"> + <!-- General page information --> + <vbox id="generalPanel"> + <table id="generalTable" xmlns="http://www.w3.org/1999/xhtml"> + <tr id="generalTitle"> + <th> + <xul:label control="titletext" data-l10n-id="general-title"/> + </th> + <td> + <input readonly="readonly" id="titletext" data-l10n-attrs="value"/> + </td> + </tr> + <tr id="generalURLRow"> + <th> + <xul:label control="urltext" data-l10n-id="general-url"/> + </th> + <td> + <input readonly="readonly" id="urltext"/> + </td> + </tr> + <tr class="tableSeparator"/> + <tr id="generalTypeRow"> + <th> + <xul:label control="typetext" data-l10n-id="general-type"/> + </th> + <td> + <input readonly="readonly" id="typetext"/> + </td> + </tr> + <tr id="generalModeRow"> + <th> + <xul:label control="modetext" data-l10n-id="general-mode"/> + </th> + <td> + <input readonly="readonly" id="modetext" data-l10n-attrs="value"/> + </td> + </tr> + <tr id="generalEncodingRow"> + <th> + <xul:label control="encodingtext" data-l10n-id="general-encoding"/> + </th> + <td> + <input readonly="readonly" id="encodingtext"/> + </td> + </tr> + <tr id="generalSizeRow"> + <th> + <xul:label control="sizetext" data-l10n-id="general-size"/> + </th> + <td> + <input readonly="readonly" id="sizetext" data-l10n-attrs="value"/> + </td> + </tr> + <tr id="generalReferrerRow"> + <th> + <xul:label control="refertext" data-l10n-id="general-referrer"/> + </th> + <td> + <input readonly="readonly" id="refertext"/> + </td> + </tr> + <tr class="tableSeparator"/> + <tr id="generalModifiedRow"> + <th> + <xul:label control="modifiedtext" data-l10n-id="general-modified"/> + </th> + <td> + <input readonly="readonly" id="modifiedtext"/> + </td> + </tr> + </table> + <separator class="thin"/> + <vbox id="metaTags" flex="1"> + <label control="metatree" id="metaTagsCaption" class="header"/> + <tree id="metatree" flex="1" hidecolumnpicker="true" contextmenu="picontext"> + <treecols> + <treecol id="meta-name" data-l10n-id="general-meta-name" + persist="width" style="flex: 1 auto;" + onclick="gMetaView.onPageMediaSort('meta-name');"/> + <splitter class="tree-splitter"/> + <treecol id="meta-content" data-l10n-id="general-meta-content" + persist="width" style="flex: 4 4 auto" + onclick="gMetaView.onPageMediaSort('meta-content');"/> + </treecols> + <treechildren id="metatreechildren" flex="1"/> + </tree> + </vbox> + <hbox pack="end"> + <button command="cmd_help" data-l10n-id="help-button" class="help-button"/> + </hbox> + </vbox> + + <!-- Media information --> + <vbox id="mediaPanel"> + <tree id="imagetree" onselect="onImageSelect();" contextmenu="picontext" + ondragstart="onBeginLinkDrag(event, 'image-address', 'image-alt')"> + <treecols> + <treecol primary="true" persist="width" style="flex: 10 10 auto" + width="10" id="image-address" data-l10n-id="media-address" + onclick="gImageView.onPageMediaSort('image-address');"/> + <splitter class="tree-splitter"/> + <treecol persist="hidden width" style="flex: 2 2 auto" + width="2" id="image-type" data-l10n-id="media-type" + onclick="gImageView.onPageMediaSort('image-type');"/> + <splitter class="tree-splitter"/> + <treecol hidden="true" persist="hidden width" style="flex: 2 2 auto" + width="2" id="image-size" data-l10n-id="media-size" value="size" + onclick="gImageView.onPageMediaSort('image-size');"/> + <splitter class="tree-splitter"/> + <treecol hidden="true" persist="hidden width" style="flex: 4 4 auto" + width="4" id="image-alt" data-l10n-id="media-alt-header" + onclick="gImageView.onPageMediaSort('image-alt');"/> + <splitter class="tree-splitter"/> + <treecol hidden="true" persist="hidden width" style="flex: 1 1 auto" + width="1" id="image-count" data-l10n-id="media-count" + onclick="gImageView.onPageMediaSort('image-count');"/> + </treecols> + <treechildren id="imagetreechildren" flex="1"/> + </tree> + <splitter orient="vertical" id="mediaSplitter" resizebefore="sibling" resizeafter="none" /> + <vbox flex="1" id="mediaPreviewBox" collapsed="true"> + <table id="mediaTable" xmlns="http://www.w3.org/1999/xhtml"> + <tr id="mediaLocationRow"> + <th> + <xul:label control="imageurltext" data-l10n-id="media-location"/> + </th> + <td> + <input readonly="readonly" id="imageurltext"/> + </td> + </tr> + <tr id="mediaTypeRow"> + <th> + <xul:label control="imagetypetext" data-l10n-id="general-type"/> + </th> + <td> + <input id="imagetypetext" data-l10n-attrs="value"/> + </td> + </tr> + <tr id="mediaSizeRow"> + <th> + <xul:label control="imagesizetext" data-l10n-id="general-size"/> + </th> + <td> + <input readonly="readonly" id="imagesizetext" data-l10n-attrs="value"/> + </td> + </tr> + <tr id="mediaDimensionRow"> + <th> + <xul:label control="imagedimensiontext" data-l10n-id="media-dimension"/> + </th> + <td> + <input readonly="readonly" id="imagedimensiontext" data-l10n-attrs="value"/> + </td> + </tr> + <tr id="mediaTextRow"> + <th> + <xul:label control="imagetext" data-l10n-id="media-text"/> + </th> + <td> + <input readonly="readonly" id="imagetext"/> + </td> + </tr> + <tr id="mediaLongdescRow"> + <th> + <xul:label control="imagelongdesctext" data-l10n-id="media-long-desc"/> + </th> + <td> + <input readonly="readonly" id="imagelongdesctext"/> + </td> + </tr> + </table> + <hbox id="imageSaveBox" align="end"> + <spacer id="imageSaveBoxSpacer" flex="1"/> + <button data-l10n-id="media-select-all" + id="selectallbutton" + oncommand="doSelectAllMedia();"/> + <button data-l10n-id="media-save-as" + id="imagesaveasbutton" + oncommand="saveMedia();"/> + </hbox> + <vbox id="imagecontainerbox" flex="1"> + <hbox id="theimagecontainer"> + <image id="thepreviewimage"/> + </hbox> + <hbox id="brokenimagecontainer" pack="center" collapsed="true"> + <image id="brokenimage" src="resource://gre-resources/broken-image.png"/> + </hbox> + </vbox> + </vbox> + <hbox id="mediaSaveBox" collapsed="true"> + <spacer id="mediaSaveBoxSpacer" flex="1"/> + <button data-l10n-id="media-save-image-as" + id="mediasaveasbutton" + oncommand="saveMedia();"/> + </hbox> + <hbox pack="end"> + <button command="cmd_help" data-l10n-id="help-button" class="help-button"/> + </hbox> + </vbox> + + <!-- Permissions --> + <vbox id="permPanel"> + <hbox id="permHostBox"> + <label data-l10n-id="permissions-for" control="hostText" /> + <html:input id="hostText" class="header" readonly="readonly"/> + </hbox> + + <vbox id="permList" flex="1"/> + <hbox pack="end"> + <button command="cmd_help" data-l10n-id="help-button" class="help-button"/> + </hbox> + </vbox> + + <!-- Security & Privacy --> + <vbox id="securityPanel"> + <!-- Identity Section --> + <groupbox> + <label class="header" data-l10n-id="security-view-identity"/> + <table xmlns="http://www.w3.org/1999/xhtml"> + <!-- Domain --> + <tr> + <th> + <xul:label data-l10n-id="security-view-identity-domain" + control="security-identity-domain-value"/> + </th> + <td> + <input id="security-identity-domain-value" readonly="readonly"/> + </td> + </tr> + <!-- Owner --> + <tr> + <th> + <xul:label id="security-identity-owner-label" + class="fieldLabel" + data-l10n-id="security-view-identity-owner" + control="security-identity-owner-value"/> + </th> + <td> + <input id="security-identity-owner-value" readonly="readonly" data-l10n-attrs="value"/> + </td> + </tr> + <!-- Verifier --> + <tr> + <th> + <xul:label data-l10n-id="security-view-identity-verifier" + control="security-identity-verifier-value"/> + </th> + <td> + <div class="table-split-column"> + <input id="security-identity-verifier-value" readonly="readonly" + data-l10n-attrs="value"/> + <xul:button id="security-view-cert" data-l10n-id="security-view" + collapsed="true" + oncommand="security.viewCert();"/> + </div> + </td> + </tr> + <!-- Certificate Validity --> + <tr id="security-identity-validity-row"> + <th> + <xul:label data-l10n-id="security-view-identity-validity" + control="security-identity-validity-value"/> + </th> + <td> + <input id="security-identity-validity-value" readonly="readonly"/> + </td> + </tr> + </table> + </groupbox> + + <!-- Privacy & History section --> + <groupbox> + <label class="header" data-l10n-id="security-view-privacy"/> + <table id="securityTable" xmlns="http://www.w3.org/1999/xhtml"> + <!-- History --> + <tr> + <th> + <xul:label control="security-privacy-history-value" data-l10n-id="security-view-privacy-history-value"/> + </th> + <td> + <xul:label id="security-privacy-history-value" + data-l10n-id="security-view-unknown"/> + </td> + </tr> + <!-- Site Data & Cookies --> + <tr id="security-privacy-sitedata-row"> + <th> + <xul:label control="security-privacy-sitedata-value" data-l10n-id="security-view-privacy-sitedata-value"/> + </th> + <td> + <div class="table-split-column"> + <xul:label id="security-privacy-sitedata-value" data-l10n-id="security-view-unknown"/> + <xul:button id="security-clear-sitedata" + disabled="true" + data-l10n-id="security-view-privacy-clearsitedata" + oncommand="security.clearSiteData();"/> + </div> + </td> + </tr> + <!-- Passwords --> + <tr> + <th> + <xul:label control="security-privacy-passwords-value" data-l10n-id="security-view-privacy-passwords-value"/> + </th> + <td> + <div class="table-split-column"> + <xul:label id="security-privacy-passwords-value" + data-l10n-id="security-view-unknown"/> + <xul:button id="security-view-password" + data-l10n-id="security-view-privacy-viewpasswords" + oncommand="security.viewPasswords();"/> + </div> + </td> + </tr> + </table> + </groupbox> + + <!-- Technical Details section --> + <groupbox> + <label class="header" data-l10n-id="security-view-technical"/> + <label id="security-technical-shortform"/> + <description id="security-technical-longform1"/> + <description id="security-technical-longform2"/> + <description id="security-technical-certificate-transparency"/> + </groupbox> + + <hbox pack="end"> + <button command="cmd_help" data-l10n-id="help-button" class="help-button"/> + </hbox> + </vbox> + <!-- Others added by overlay --> + </deck> + +</window> diff --git a/browser/base/content/pageinfo/permissions.js b/browser/base/content/pageinfo/permissions.js new file mode 100644 index 0000000000..7834e27c98 --- /dev/null +++ b/browser/base/content/pageinfo/permissions.js @@ -0,0 +1,240 @@ +/* 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 pageInfo.js */ + +const { SitePermissions } = ChromeUtils.importESModule( + "resource:///modules/SitePermissions.sys.mjs" +); + +var gPermPrincipal; + +// List of ids of permissions to hide. +const EXCLUDE_PERMS = ["open-protocol-handler"]; + +// Array of permissionIDs sorted alphabetically by label. +let gPermissions = SitePermissions.listPermissions() + .filter(permissionID => { + if (!SitePermissions.getPermissionLabel(permissionID)) { + return false; + } + return !EXCLUDE_PERMS.includes(permissionID); + }) + .sort((a, b) => { + let firstLabel = SitePermissions.getPermissionLabel(a); + let secondLabel = SitePermissions.getPermissionLabel(b); + return firstLabel.localeCompare(secondLabel); + }); + +var permissionObserver = { + observe(aSubject, aTopic, aData) { + if (aTopic == "perm-changed") { + var permission = aSubject.QueryInterface(Ci.nsIPermission); + if ( + permission.matches(gPermPrincipal, true) && + gPermissions.includes(permission.type) + ) { + initRow(permission.type); + } + } + }, +}; + +function getExcludedPermissions() { + return EXCLUDE_PERMS; +} + +function onLoadPermission(uri, principal) { + var permTab = document.getElementById("permTab"); + if (SitePermissions.isSupportedPrincipal(principal)) { + gPermPrincipal = principal; + var hostText = document.getElementById("hostText"); + hostText.value = uri.displayPrePath; + + for (var i of gPermissions) { + initRow(i); + } + Services.obs.addObserver(permissionObserver, "perm-changed"); + window.addEventListener("unload", onUnloadPermission); + permTab.hidden = false; + } else { + permTab.hidden = true; + } +} + +function onUnloadPermission() { + Services.obs.removeObserver(permissionObserver, "perm-changed"); +} + +function initRow(aPartId) { + createRow(aPartId); + + var checkbox = document.getElementById(aPartId + "Def"); + var command = document.getElementById("cmd_" + aPartId + "Toggle"); + var { state, scope } = SitePermissions.getForPrincipal( + gPermPrincipal, + aPartId + ); + let defaultState = SitePermissions.getDefault(aPartId); + + // Since cookies preferences have many different possible configuration states + // we don't consider any permission except "no permission" to be default. + if (aPartId == "cookie") { + state = Services.perms.testPermissionFromPrincipal( + gPermPrincipal, + "cookie" + ); + + if (state == SitePermissions.UNKNOWN) { + checkbox.checked = true; + command.setAttribute("disabled", "true"); + // Don't select any item in the radio group, as we can't + // confidently say that all cookies on the site will be allowed. + let radioGroup = document.getElementById("cookieRadioGroup"); + radioGroup.selectedItem = null; + } else { + checkbox.checked = false; + command.removeAttribute("disabled"); + } + + setRadioState(aPartId, state); + + checkbox.disabled = Services.prefs.prefIsLocked( + "network.cookie.cookieBehavior" + ); + + return; + } + + if (state != defaultState) { + checkbox.checked = false; + command.removeAttribute("disabled"); + } else { + checkbox.checked = true; + command.setAttribute("disabled", "true"); + } + + if ( + [SitePermissions.SCOPE_POLICY, SitePermissions.SCOPE_GLOBAL].includes(scope) + ) { + checkbox.setAttribute("disabled", "true"); + command.setAttribute("disabled", "true"); + } + + setRadioState(aPartId, state); + + switch (aPartId) { + case "install": + checkbox.disabled = !Services.policies.isAllowed("xpinstall"); + break; + case "popup": + checkbox.disabled = Services.prefs.prefIsLocked( + "dom.disable_open_during_load" + ); + break; + case "autoplay-media": + checkbox.disabled = Services.prefs.prefIsLocked("media.autoplay.default"); + break; + case "geo": + case "desktop-notification": + case "camera": + case "microphone": + case "xr": + checkbox.disabled = Services.prefs.prefIsLocked( + "permissions.default." + aPartId + ); + break; + } +} + +function createRow(aPartId) { + let rowId = "perm-" + aPartId + "-row"; + if (document.getElementById(rowId)) { + return; + } + + let commandId = "cmd_" + aPartId + "Toggle"; + let labelId = "perm-" + aPartId + "-label"; + let radiogroupId = aPartId + "RadioGroup"; + + let command = document.createXULElement("command"); + command.setAttribute("id", commandId); + command.setAttribute("oncommand", "onRadioClick('" + aPartId + "');"); + document.getElementById("pageInfoCommandSet").appendChild(command); + + let row = document.createXULElement("vbox"); + row.setAttribute("id", rowId); + row.setAttribute("class", "permission"); + + let label = document.createXULElement("label"); + label.setAttribute("id", labelId); + label.setAttribute("control", radiogroupId); + label.setAttribute("value", SitePermissions.getPermissionLabel(aPartId)); + label.setAttribute("class", "permissionLabel"); + row.appendChild(label); + + let controls = document.createXULElement("hbox"); + controls.setAttribute("role", "group"); + controls.setAttribute("aria-labelledby", labelId); + + let checkbox = document.createXULElement("checkbox"); + checkbox.setAttribute("id", aPartId + "Def"); + checkbox.setAttribute("oncommand", "onCheckboxClick('" + aPartId + "');"); + checkbox.setAttribute("native", true); + document.l10n.setAttributes(checkbox, "permissions-use-default"); + controls.appendChild(checkbox); + + let spacer = document.createXULElement("spacer"); + spacer.setAttribute("flex", "1"); + controls.appendChild(spacer); + + let radiogroup = document.createXULElement("radiogroup"); + radiogroup.setAttribute("id", radiogroupId); + radiogroup.setAttribute("orient", "horizontal"); + for (let state of SitePermissions.getAvailableStates(aPartId)) { + let radio = document.createXULElement("radio"); + radio.setAttribute("id", aPartId + "#" + state); + radio.setAttribute( + "label", + SitePermissions.getMultichoiceStateLabel(aPartId, state) + ); + radio.setAttribute("command", commandId); + radiogroup.appendChild(radio); + } + controls.appendChild(radiogroup); + + row.appendChild(controls); + + document.getElementById("permList").appendChild(row); +} + +function onCheckboxClick(aPartId) { + var command = document.getElementById("cmd_" + aPartId + "Toggle"); + var checkbox = document.getElementById(aPartId + "Def"); + if (checkbox.checked) { + SitePermissions.removeFromPrincipal(gPermPrincipal, aPartId); + command.setAttribute("disabled", "true"); + } else { + onRadioClick(aPartId); + command.removeAttribute("disabled"); + } +} + +function onRadioClick(aPartId) { + var radioGroup = document.getElementById(aPartId + "RadioGroup"); + let permission; + if (radioGroup.selectedItem) { + permission = parseInt(radioGroup.selectedItem.id.split("#")[1]); + } else { + permission = SitePermissions.getDefault(aPartId); + } + SitePermissions.setForPrincipal(gPermPrincipal, aPartId, permission); +} + +function setRadioState(aPartId, aValue) { + var radio = document.getElementById(aPartId + "#" + aValue); + if (radio) { + radio.radioGroup.selectedItem = radio; + } +} diff --git a/browser/base/content/pageinfo/security.js b/browser/base/content/pageinfo/security.js new file mode 100644 index 0000000000..e4d52f889f --- /dev/null +++ b/browser/base/content/pageinfo/security.js @@ -0,0 +1,426 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +const { SiteDataManager } = ChromeUtils.import( + "resource:///modules/SiteDataManager.jsm" +); +const { DownloadUtils } = ChromeUtils.importESModule( + "resource://gre/modules/DownloadUtils.sys.mjs" +); + +/* import-globals-from pageInfo.js */ + +ChromeUtils.defineESModuleGetters(this, { + LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs", + PluralForm: "resource://gre/modules/PluralForm.sys.mjs", +}); + +var security = { + async init(uri, windowInfo) { + this.uri = uri; + this.windowInfo = windowInfo; + this.securityInfo = await this._getSecurityInfo(); + }, + + viewCert() { + let certChain = this.securityInfo.certChain; + let certs = certChain.map(elem => + encodeURIComponent(elem.getBase64DERString()) + ); + let certsStringURL = certs.map(elem => `cert=${elem}`); + certsStringURL = certsStringURL.join("&"); + let url = `about:certificate?${certsStringURL}`; + let win = BrowserWindowTracker.getTopWindow(); + win.switchToTabHavingURI(url, true, {}); + }, + + async _getSecurityInfo() { + // We don't have separate info for a frame, return null until further notice + // (see bug 138479) + if (!this.windowInfo.isTopWindow) { + return null; + } + + var ui = security._getSecurityUI(); + if (!ui) { + return null; + } + + var isBroken = ui.state & Ci.nsIWebProgressListener.STATE_IS_BROKEN; + var isMixed = + ui.state & + (Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT | + Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT); + var isEV = ui.state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL; + + let retval = { + cAName: "", + encryptionAlgorithm: "", + encryptionStrength: 0, + version: "", + isBroken, + isMixed, + isEV, + cert: null, + certificateTransparency: null, + }; + + // Only show certificate info for secure contexts. This prevents us from + // showing certificate data for http origins when using a proxy. + // https://searchfox.org/mozilla-central/rev/9c72508fcf2bba709a5b5b9eae9da35e0c707baa/security/manager/ssl/nsSecureBrowserUI.cpp#62-64 + if (!ui.isSecureContext) { + return retval; + } + + let secInfo = ui.secInfo; + if (!secInfo) { + return retval; + } + + let cert = secInfo.serverCert; + let issuerName = null; + if (cert) { + issuerName = cert.issuerOrganization || cert.issuerName; + } + + let certChainArray = []; + if (secInfo.succeededCertChain.length) { + certChainArray = secInfo.succeededCertChain; + } else { + certChainArray = secInfo.failedCertChain; + } + + retval = { + cAName: issuerName, + encryptionAlgorithm: undefined, + encryptionStrength: undefined, + version: undefined, + isBroken, + isMixed, + isEV, + cert, + certChain: certChainArray, + certificateTransparency: undefined, + }; + + var version; + try { + retval.encryptionAlgorithm = secInfo.cipherName; + retval.encryptionStrength = secInfo.secretKeyLength; + version = secInfo.protocolVersion; + } catch (e) {} + + switch (version) { + case Ci.nsITransportSecurityInfo.SSL_VERSION_3: + retval.version = "SSL 3"; + break; + case Ci.nsITransportSecurityInfo.TLS_VERSION_1: + retval.version = "TLS 1.0"; + break; + case Ci.nsITransportSecurityInfo.TLS_VERSION_1_1: + retval.version = "TLS 1.1"; + break; + case Ci.nsITransportSecurityInfo.TLS_VERSION_1_2: + retval.version = "TLS 1.2"; + break; + case Ci.nsITransportSecurityInfo.TLS_VERSION_1_3: + retval.version = "TLS 1.3"; + break; + } + + // Select the status text to display for Certificate Transparency. + // Since we do not yet enforce the CT Policy on secure connections, + // we must not complain on policy discompliance (it might be viewed + // as a security issue by the user). + switch (secInfo.certificateTransparencyStatus) { + case Ci.nsITransportSecurityInfo.CERTIFICATE_TRANSPARENCY_NOT_APPLICABLE: + case Ci.nsITransportSecurityInfo + .CERTIFICATE_TRANSPARENCY_POLICY_NOT_ENOUGH_SCTS: + case Ci.nsITransportSecurityInfo + .CERTIFICATE_TRANSPARENCY_POLICY_NOT_DIVERSE_SCTS: + retval.certificateTransparency = null; + break; + case Ci.nsITransportSecurityInfo + .CERTIFICATE_TRANSPARENCY_POLICY_COMPLIANT: + retval.certificateTransparency = "Compliant"; + break; + } + + return retval; + }, + + // Find the secureBrowserUI object (if present) + _getSecurityUI() { + if (window.opener.gBrowser) { + return window.opener.gBrowser.securityUI; + } + return null; + }, + + async _updateSiteDataInfo() { + // Save site data info for deleting. + this.siteData = await SiteDataManager.getSite(this.uri.host); + + let clearSiteDataButton = document.getElementById( + "security-clear-sitedata" + ); + let siteDataLabel = document.getElementById( + "security-privacy-sitedata-value" + ); + + if (!this.siteData) { + document.l10n.setAttributes(siteDataLabel, "security-site-data-no"); + clearSiteDataButton.setAttribute("disabled", "true"); + return; + } + + let { usage } = this.siteData; + if (usage > 0) { + let size = DownloadUtils.convertByteUnits(usage); + if (this.siteData.cookies.length) { + document.l10n.setAttributes( + siteDataLabel, + "security-site-data-cookies", + { value: size[0], unit: size[1] } + ); + } else { + document.l10n.setAttributes(siteDataLabel, "security-site-data-only", { + value: size[0], + unit: size[1], + }); + } + } else { + // We're storing cookies, else getSite would have returned null. + document.l10n.setAttributes( + siteDataLabel, + "security-site-data-cookies-only" + ); + } + + clearSiteDataButton.removeAttribute("disabled"); + }, + + /** + * Clear Site Data and Cookies + */ + clearSiteData() { + if (this.siteData) { + let { baseDomain } = this.siteData; + if (SiteDataManager.promptSiteDataRemoval(window, [baseDomain])) { + SiteDataManager.remove(baseDomain).then(() => + this._updateSiteDataInfo() + ); + } + } + }, + + /** + * Open the login manager window + */ + viewPasswords() { + LoginHelper.openPasswordManager(window, { + filterString: this.windowInfo.hostName, + entryPoint: "pageinfo", + }); + }, +}; + +async function securityOnLoad(uri, windowInfo) { + await security.init(uri, windowInfo); + + let info = security.securityInfo; + if ( + !info || + (uri.scheme === "about" && !uri.spec.startsWith("about:certerror")) + ) { + document.getElementById("securityTab").hidden = true; + return; + } + document.getElementById("securityTab").hidden = false; + + /* Set Identity section text */ + setText("security-identity-domain-value", windowInfo.hostName); + + var validity; + if (info.cert && !info.isBroken) { + validity = info.cert.validity.notAfterLocalDay; + + // Try to pull out meaningful values. Technically these fields are optional + // so we'll employ fallbacks where appropriate. The EV spec states that Org + // fields must be specified for subject and issuer so that case is simpler. + if (info.isEV) { + setText("security-identity-owner-value", info.cert.organization); + setText("security-identity-verifier-value", info.cAName); + } else { + // Technically, a non-EV cert might specify an owner in the O field or not, + // depending on the CA's issuing policies. However we don't have any programmatic + // way to tell those apart, and no policy way to establish which organization + // vetting standards are good enough (that's what EV is for) so we default to + // treating these certs as domain-validated only. + document.l10n.setAttributes( + document.getElementById("security-identity-owner-value"), + "page-info-security-no-owner" + ); + setText( + "security-identity-verifier-value", + info.cAName || info.cert.issuerCommonName || info.cert.issuerName + ); + } + } else { + // We don't have valid identity credentials. + document.l10n.setAttributes( + document.getElementById("security-identity-owner-value"), + "page-info-security-no-owner" + ); + document.l10n.setAttributes( + document.getElementById("security-identity-verifier-value"), + "page-info-not-specified" + ); + } + + if (validity) { + setText("security-identity-validity-value", validity); + } else { + document.getElementById("security-identity-validity-row").hidden = true; + } + + /* Manage the View Cert button*/ + var viewCert = document.getElementById("security-view-cert"); + if (info.cert) { + viewCert.collapsed = false; + } else { + viewCert.collapsed = true; + } + + /* Set Privacy & History section text */ + + // Only show quota usage data for websites, not internal sites. + if (uri.scheme == "http" || uri.scheme == "https") { + SiteDataManager.updateSites().then(() => security._updateSiteDataInfo()); + } else { + document.getElementById("security-privacy-sitedata-row").hidden = true; + } + + if (realmHasPasswords(uri)) { + document.l10n.setAttributes( + document.getElementById("security-privacy-passwords-value"), + "saved-passwords-yes" + ); + } else { + document.l10n.setAttributes( + document.getElementById("security-privacy-passwords-value"), + "saved-passwords-no" + ); + } + + document.l10n.setAttributes( + document.getElementById("security-privacy-history-value"), + "security-visits-number", + { visits: previousVisitCount(windowInfo.hostName) } + ); + + /* Set the Technical Detail section messages */ + const pkiBundle = document.getElementById("pkiBundle"); + var hdr; + var msg1; + var msg2; + + if (info.isBroken) { + if (info.isMixed) { + hdr = pkiBundle.getString("pageInfo_MixedContent"); + msg1 = pkiBundle.getString("pageInfo_MixedContent2"); + } else { + hdr = pkiBundle.getFormattedString("pageInfo_BrokenEncryption", [ + info.encryptionAlgorithm, + info.encryptionStrength + "", + info.version, + ]); + msg1 = pkiBundle.getString("pageInfo_WeakCipher"); + } + msg2 = pkiBundle.getString("pageInfo_Privacy_None2"); + } else if (info.encryptionStrength > 0) { + hdr = pkiBundle.getFormattedString( + "pageInfo_EncryptionWithBitsAndProtocol", + [info.encryptionAlgorithm, info.encryptionStrength + "", info.version] + ); + msg1 = pkiBundle.getString("pageInfo_Privacy_Encrypted1"); + msg2 = pkiBundle.getString("pageInfo_Privacy_Encrypted2"); + } else { + hdr = pkiBundle.getString("pageInfo_NoEncryption"); + if (windowInfo.hostName != null) { + msg1 = pkiBundle.getFormattedString("pageInfo_Privacy_None1", [ + windowInfo.hostName, + ]); + } else { + msg1 = pkiBundle.getString("pageInfo_Privacy_None4"); + } + msg2 = pkiBundle.getString("pageInfo_Privacy_None2"); + } + setText("security-technical-shortform", hdr); + setText("security-technical-longform1", msg1); + setText("security-technical-longform2", msg2); + + const ctStatus = document.getElementById( + "security-technical-certificate-transparency" + ); + if (info.certificateTransparency) { + ctStatus.hidden = false; + ctStatus.value = pkiBundle.getString( + "pageInfo_CertificateTransparency_" + info.certificateTransparency + ); + } else { + ctStatus.hidden = true; + } +} + +function setText(id, value) { + var element = document.getElementById(id); + if (!element) { + return; + } + if (element.localName == "input" || element.localName == "label") { + element.value = value; + } else { + element.textContent = value; + } +} + +/** + * Return true iff realm (proto://host:port) (extracted from uri) has + * saved passwords + */ +function realmHasPasswords(uri) { + return Services.logins.countLogins(uri.prePath, "", "") > 0; +} + +/** + * Return the number of previous visits recorded for host before today. + * + * @param host - the domain name to look for in history + */ +function previousVisitCount(host, endTimeReference) { + if (!host) { + return 0; + } + + var historyService = Cc[ + "@mozilla.org/browser/nav-history-service;1" + ].getService(Ci.nsINavHistoryService); + + var options = historyService.getNewQueryOptions(); + options.resultType = options.RESULTS_AS_VISIT; + + // Search for visits to this host before today + var query = historyService.getNewQuery(); + query.endTimeReference = query.TIME_RELATIVE_TODAY; + query.endTime = 0; + query.domain = host; + + var result = historyService.executeQuery(query, options); + result.root.containerOpen = true; + var cc = result.root.childCount; + result.root.containerOpen = false; + return cc; +} |