/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */ /* 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 { interfaces: Ci, utils: Cu, results: Cr } = Components; ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); ChromeUtils.import("resource://gre/modules/Services.jsm"); ChromeUtils.defineModuleGetter(this, "SiteDataManager", "resource:///modules/SiteDataManager.jsm"); ChromeUtils.defineModuleGetter(this, "DownloadUtils", "resource://gre/modules/DownloadUtils.jsm"); "use strict"; let gSiteDataSettings = { // Array of metadata of sites. Each array element is object holding: // - uri: uri of site; instance of nsIURI // - status: persistent-storage permission status // - usage: disk usage which site uses // - userAction: "remove" or "update-permission"; the action user wants to take. // If not specified, means no action to take _sites: null, _list: null, _searchBox: null, _prefStrBundle: null, init() { function setEventListener(id, eventType, callback) { document.getElementById(id) .addEventListener(eventType, callback.bind(gSiteDataSettings)); } this._list = document.getElementById("sitesList"); this._searchBox = document.getElementById("searchBox"); this._prefStrBundle = document.getElementById("bundlePreferences"); SiteDataManager.getSites().then(sites => { this._sites = sites; let sortCol = document.querySelector("treecol[data-isCurrentSortCol=true]"); this._sortSites(this._sites, sortCol); this._buildSitesList(this._sites); Services.obs.notifyObservers(null, "sitedata-settings-init"); }); let brandShortName = document.getElementById("bundle_brand").getString("brandShortName"); let settingsDescription = document.getElementById("settingsDescription"); settingsDescription.textContent = this._prefStrBundle.getFormattedString("siteDataSettings2.description", [brandShortName]); setEventListener("hostCol", "click", this.onClickTreeCol); setEventListener("usageCol", "click", this.onClickTreeCol); setEventListener("statusCol", "click", this.onClickTreeCol); setEventListener("cancel", "command", this.close); setEventListener("save", "command", this.saveChanges); setEventListener("searchBox", "command", this.onCommandSearch); setEventListener("removeAll", "command", this.onClickRemoveAll); setEventListener("removeSelected", "command", this.onClickRemoveSelected); }, _updateButtonsState() { let items = this._list.getElementsByTagName("richlistitem"); let removeSelectedBtn = document.getElementById("removeSelected"); let removeAllBtn = document.getElementById("removeAll"); removeSelectedBtn.disabled = items.length == 0; removeAllBtn.disabled = removeSelectedBtn.disabled; let removeAllBtnLabelStringID = "removeAllSiteData.label"; let removeAllBtnAccesskeyStringID = "removeAllSiteData.accesskey"; if (this._searchBox.value) { removeAllBtnLabelStringID = "removeAllSiteDataShown.label"; removeAllBtnAccesskeyStringID = "removeAllSiteDataShown.accesskey"; } removeAllBtn.setAttribute("label", this._prefStrBundle.getString(removeAllBtnLabelStringID)); removeAllBtn.setAttribute("accesskey", this._prefStrBundle.getString(removeAllBtnAccesskeyStringID)); }, /** * @param sites {Array} * @param col {XULElement} the being sorted on */ _sortSites(sites, col) { let isCurrentSortCol = col.getAttribute("data-isCurrentSortCol"); let sortDirection = col.getAttribute("data-last-sortDirection") || "ascending"; if (isCurrentSortCol) { // Sort on the current column, flip the sorting direction sortDirection = sortDirection === "ascending" ? "descending" : "ascending"; } let sortFunc = null; switch (col.id) { case "hostCol": sortFunc = (a, b) => { let aHost = a.host.toLowerCase(); let bHost = b.host.toLowerCase(); return aHost.localeCompare(bHost); }; break; case "statusCol": sortFunc = (a, b) => { if (a.persisted && !b.persisted) { return 1; } else if (!a.persisted && b.persisted) { return -1; } return 0; }; break; case "usageCol": sortFunc = (a, b) => a.usage - b.usage; break; } if (sortDirection === "descending") { sites.sort((a, b) => sortFunc(b, a)); } else { sites.sort(sortFunc); } let cols = this._list.querySelectorAll("treecol"); cols.forEach(c => { c.removeAttribute("sortDirection"); c.removeAttribute("data-isCurrentSortCol"); }); col.setAttribute("data-isCurrentSortCol", true); col.setAttribute("sortDirection", sortDirection); col.setAttribute("data-last-sortDirection", sortDirection); }, /** * @param sites {Array} array of metadata of sites */ _buildSitesList(sites) { // Clear old entries. let oldItems = this._list.querySelectorAll("richlistitem"); for (let item of oldItems) { item.remove(); } let keyword = this._searchBox.value.toLowerCase().trim(); for (let site of sites) { let host = site.host; if (keyword && !host.includes(keyword)) { continue; } if (site.userAction === "remove") { continue; } let size = DownloadUtils.convertByteUnits(site.usage); let item = document.createElement("richlistitem"); item.setAttribute("host", host); item.setAttribute("usage", this._prefStrBundle.getFormattedString("siteUsage", size)); if (site.persisted) { item.setAttribute("status", this._prefStrBundle.getString("persistent")); } this._list.appendChild(item); } this._updateButtonsState(); }, _removeSiteItems(items) { for (let i = items.length - 1; i >= 0; --i) { let item = items[i]; let host = item.getAttribute("host"); let siteForHost = this._sites.find(site => site.host == host); if (siteForHost) { siteForHost.userAction = "remove"; } item.remove(); } this._updateButtonsState(); }, _getBaseDomainFromHost(host) { let result = host; try { result = Services.eTLD.getBaseDomainFromHost(host); } catch (e) { if (e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS || e.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) { // For this 2 expected errors, just take the host as the result. // - NS_ERROR_HOST_IS_IP_ADDRESS: the host is in ipv4/ipv6. // - NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS: not enough domain part to extract. result = host; } else { throw e; } } return result; }, saveChanges() { let allowed = true; // Confirm user really wants to remove site data starts let removals = new Set(); this._sites = this._sites.filter(site => { if (site.userAction === "remove") { removals.add(site.host); return false; } return true; }); if (removals.size > 0) { if (this._sites.length == 0) { // User selects all sites so equivalent to clearing all data let flags = Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 + Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1 + Services.prompt.BUTTON_POS_0_DEFAULT; let prefStrBundle = document.getElementById("bundlePreferences"); let title = prefStrBundle.getString("clearSiteDataPromptTitle"); let text = prefStrBundle.getString("clearSiteDataPromptText"); let btn0Label = prefStrBundle.getString("clearSiteDataNow"); let result = Services.prompt.confirmEx(window, title, text, flags, btn0Label, null, null, null, {}); allowed = result == 0; if (allowed) { SiteDataManager.removeAll(); } } else { // User only removes partial sites. // We will remove cookies based on base domain, say, user selects "news.foo.com" to remove. // The cookies under "music.foo.com" will be removed together. // We have to prompt user about this action. let hostsTable = new Map(); // Group removed sites by base domain for (let host of removals) { let baseDomain = this._getBaseDomainFromHost(host); let hosts = hostsTable.get(baseDomain); if (!hosts) { hosts = []; hostsTable.set(baseDomain, hosts); } hosts.push(host); } // Pick out sites with the same base domain as removed sites for (let site of this._sites) { let baseDomain = this._getBaseDomainFromHost(site.host); let hosts = hostsTable.get(baseDomain); if (hosts) { hosts.push(site.host); } } let args = { hostsTable, allowed: false }; let features = "centerscreen,chrome,modal,resizable=no"; window.openDialog("chrome://browser/content/preferences/siteDataRemoveSelected.xul", "", features, args); allowed = args.allowed; if (allowed) { try { SiteDataManager.remove(removals); } catch (e) { // Hit error, maybe remove unknown site. // Let's print out the error, then proceed to close this settings dialog. // When we next open again we will once more get sites from the SiteDataManager and refresh the list. Cu.reportError(e); } } } } // Confirm user really wants to remove site data ends this.close(); }, close() { window.close(); }, onClickTreeCol(e) { this._sortSites(this._sites, e.target); this._buildSitesList(this._sites); }, onCommandSearch() { this._buildSitesList(this._sites); }, onClickRemoveSelected() { let selected = this._list.selectedItem; if (selected) { this._removeSiteItems([selected]); } }, onClickRemoveAll() { let siteItems = this._list.getElementsByTagName("richlistitem"); if (siteItems.length > 0) { this._removeSiteItems(siteItems); } }, onKeyPress(e) { if (e.keyCode == KeyEvent.DOM_VK_ESCAPE) { this.close(); } } };