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