diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index d7fb454745c0..fb1109df5947 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1015,6 +1015,8 @@ pref("security.sandbox.content.level", 1); // This setting may not be required anymore once we decide to permanently // enable the content sandbox. pref("security.sandbox.content.level", 2); +pref("security.sandbox.content.write_path_whitelist", ""); +pref("security.sandbox.content.syscall_whitelist", ""); #endif #if defined(XP_MACOSX) || defined(XP_WIN) diff --git a/browser/components/preferences/SiteDataManager.jsm b/browser/components/preferences/SiteDataManager.jsm index a86aafef0168..6c8cf1e06f4e 100644 --- a/browser/components/preferences/SiteDataManager.jsm +++ b/browser/components/preferences/SiteDataManager.jsm @@ -8,6 +8,8 @@ Cu.import("resource://gre/modules/NetUtil.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "OfflineAppCacheHelper", "resource:///modules/offlineAppCache.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService", + "resource://gre/modules/ContextualIdentityService.jsm"); this.EXPORTED_SYMBOLS = [ "SiteDataManager" @@ -29,6 +31,7 @@ this.SiteDataManager = { // - quotaUsage: the usage of indexedDB and localStorage. // - appCacheList: an array of app cache; instances of nsIApplicationCache // - diskCacheList: an array. Each element is object holding metadata of http cache: + // - uri: the uri of that http cache // - dataSize: that http cache size // - idEnhance: the id extension of that http cache _sites: new Map(), @@ -53,7 +56,7 @@ this.SiteDataManager = { status = Services.perms.testExactPermissionFromPrincipal(perm.principal, "persistent-storage"); if (status === Ci.nsIPermissionManager.ALLOW_ACTION || status === Ci.nsIPermissionManager.DENY_ACTION) { - this._sites.set(perm.principal.origin, { + this._sites.set(perm.principal.URI.spec, { perm, status, quotaUsage: 0, @@ -125,6 +128,7 @@ this.SiteDataManager = { for (let site of sites.values()) { if (site.perm.matchesURI(uri, true)) { site.diskCacheList.push({ + uri, dataSize, idEnhance }); @@ -161,25 +165,6 @@ this.SiteDataManager = { }); }, - _removePermission(site) { - Services.perms.removePermission(site.perm); - }, - - _removeQuotaUsage(site) { - this._qms.clearStoragesForPrincipal(site.perm.principal, null, true); - }, - - removeAll() { - for (let site of this._sites.values()) { - this._removePermission(site); - this._removeQuotaUsage(site); - } - Services.cache2.clear(); - Services.cookies.removeAll(); - OfflineAppCacheHelper.clear(); - this.updateSites(); - }, - getSites() { return Promise.all([this._updateQuotaPromise, this._updateDiskCachePromise]) .then(() => { @@ -201,5 +186,70 @@ this.SiteDataManager = { } return list; }); + }, + + _removePermission(site) { + Services.perms.removePermission(site.perm); + }, + + _removeQuotaUsage(site) { + this._qms.clearStoragesForPrincipal(site.perm.principal, null, true); + }, + + _removeDiskCache(site) { + for (let cache of site.diskCacheList) { + this._diskCache.asyncDoomURI(cache.uri, cache.idEnhance, null); + } + }, + + _removeAppCache(site) { + for (let cache of site.appCacheList) { + cache.discard(); + } + }, + + _removeCookie(site) { + let host = site.perm.principal.URI.host; + let e = Services.cookies.getCookiesFromHost(host, {}); + while (e.hasMoreElements()) { + let cookie = e.getNext(); + if (cookie instanceof Components.interfaces.nsICookie) { + if (this.isPrivateCookie(cookie)) { + continue; + } + Services.cookies.remove( + cookie.host, cookie.name, cookie.path, false, cookie.originAttributes); + } + } + }, + + remove(uris) { + for (let uri of uris) { + let site = this._sites.get(uri.spec); + if (site) { + this._removePermission(site); + this._removeQuotaUsage(site); + this._removeDiskCache(site); + this._removeAppCache(site); + this._removeCookie(site); + } + } + this.updateSites(); + }, + + removeAll() { + for (let site of this._sites.values()) { + this._removePermission(site); + this._removeQuotaUsage(site); + } + Services.cache2.clear(); + Services.cookies.removeAll(); + OfflineAppCacheHelper.clear(); + this.updateSites(); + }, + + isPrivateCookie(cookie) { + let { userContextId } = cookie.originAttributes; + return userContextId && !ContextualIdentityService.getIdentityFromId(userContextId).public; } }; diff --git a/browser/components/preferences/cookies.js b/browser/components/preferences/cookies.js index 778efaefc12b..29332dcb3d66 100644 --- a/browser/components/preferences/cookies.js +++ b/browser/components/preferences/cookies.js @@ -10,6 +10,8 @@ Components.utils.import("resource://gre/modules/PluralForm.jsm"); Components.utils.import("resource://gre/modules/Services.jsm") Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "SiteDataManager", + "resource:///modules/SiteDataManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService", "resource://gre/modules/ContextualIdentityService.jsm"); @@ -76,21 +78,12 @@ var gCookiesWindow = { aCookieB.originAttributes); }, - _isPrivateCookie(aCookie) { - let { userContextId } = aCookie.originAttributes; - if (!userContextId) { - // Default identity is public. - return false; - } - return !ContextualIdentityService.getIdentityFromId(userContextId).public; - }, - observe(aCookie, aTopic, aData) { if (aTopic != "cookie-changed") return; if (aCookie instanceof Components.interfaces.nsICookie) { - if (this._isPrivateCookie(aCookie)) { + if (SiteDataManager.isPrivateCookie(aCookie)) { return; } @@ -484,7 +477,7 @@ var gCookiesWindow = { while (e.hasMoreElements()) { var cookie = e.getNext(); if (cookie && cookie instanceof Components.interfaces.nsICookie) { - if (this._isPrivateCookie(cookie)) { + if (SiteDataManager.isPrivateCookie(cookie)) { continue; } diff --git a/browser/components/preferences/in-content/sync.xul b/browser/components/preferences/in-content/sync.xul old mode 100644 new mode 100755 diff --git a/browser/components/preferences/in-content/tests/browser_advanced_siteData.js b/browser/components/preferences/in-content/tests/browser_advanced_siteData.js index ea9e73186075..99e8743744b6 100644 --- a/browser/components/preferences/in-content/tests/browser_advanced_siteData.js +++ b/browser/components/preferences/in-content/tests/browser_advanced_siteData.js @@ -115,6 +115,12 @@ function addPersistentStoragePerm(origin) { Services.perms.addFromPrincipal(principal, "persistent-storage", Ci.nsIPermissionManager.ALLOW_ACTION); } +function removePersistentStoragePerm(origin) { + let uri = NetUtil.newURI(origin); + let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {}); + Services.perms.removeFromPrincipal(principal, "persistent-storage"); +} + function getPersistentStoragePermStatus(origin) { let uri = NetUtil.newURI(origin); let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {}); @@ -144,6 +150,33 @@ function getCacheUsage() { }); } +function openSettingsDialog() { + let doc = gBrowser.selectedBrowser.contentDocument; + let settingsBtn = doc.getElementById("siteDataSettings"); + let dialogOverlay = doc.getElementById("dialogOverlay"); + let dialogLoadPromise = promiseLoadSubDialog("chrome://browser/content/preferences/siteDataSettings.xul"); + let dialogInitPromise = TestUtils.topicObserved("sitedata-settings-init", () => true); + let fullyLoadPromise = Promise.all([ dialogLoadPromise, dialogInitPromise ]).then(() => { + is(dialogOverlay.style.visibility, "visible", "The Settings dialog should be visible"); + }); + settingsBtn.doCommand(); + return fullyLoadPromise; +} + +function promiseSettingsDialogClose() { + return new Promise(resolve => { + let doc = gBrowser.selectedBrowser.contentDocument; + let dialogOverlay = doc.getElementById("dialogOverlay"); + let win = content.gSubDialog._frame.contentWindow; + win.addEventListener("unload", function unload() { + if (win.document.documentURI === "chrome://browser/content/preferences/siteDataSettings.xul") { + isnot(dialogOverlay.style.visibility, "visible", "The Settings dialog should be hidden"); + resolve(); + } + }, { once: true }); + }); +} + function promiseSitesUpdated() { return TestUtils.topicObserved("sitedatamanager:sites-updated", () => true); } @@ -237,16 +270,9 @@ add_task(function* () { let updatePromise = promiseSitesUpdated(); yield openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true }); yield updatePromise; + yield openSettingsDialog(); - // Open the siteDataSettings subdialog let doc = gBrowser.selectedBrowser.contentDocument; - let settingsBtn = doc.getElementById("siteDataSettings"); - let dialogOverlay = doc.getElementById("dialogOverlay"); - let dialogPromise = promiseLoadSubDialog("chrome://browser/content/preferences/siteDataSettings.xul"); - settingsBtn.doCommand(); - yield dialogPromise; - is(dialogOverlay.style.visibility, "visible", "The dialog should be visible"); - let dialogFrame = doc.getElementById("dialogFrame"); let frameDoc = dialogFrame.contentDocument; let hostCol = frameDoc.getElementById("hostCol"); @@ -335,16 +361,9 @@ add_task(function* () { let updatePromise = promiseSitesUpdated(); yield openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true }); yield updatePromise; + yield openSettingsDialog(); - // Open the siteDataSettings subdialog let doc = gBrowser.selectedBrowser.contentDocument; - let settingsBtn = doc.getElementById("siteDataSettings"); - let dialogOverlay = doc.getElementById("dialogOverlay"); - let dialogPromise = promiseLoadSubDialog("chrome://browser/content/preferences/siteDataSettings.xul"); - settingsBtn.doCommand(); - yield dialogPromise; - is(dialogOverlay.style.visibility, "visible", "The dialog should be visible"); - let frameDoc = doc.getElementById("dialogFrame").contentDocument; let searchBox = frameDoc.getElementById("searchBox"); let mockOrigins = Array.from(mockSiteDataManager.sites.keys()); @@ -374,3 +393,202 @@ add_task(function* () { }); } }); + +// Test selecting and removing all sites one by one +add_task(function* () { + yield SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]}); + let fakeOrigins = [ + "https://news.foo.com/", + "https://mails.bar.com/", + "https://videos.xyz.com/", + "https://books.foo.com/", + "https://account.bar.com/", + "https://shopping.xyz.com/" + ]; + fakeOrigins.forEach(origin => addPersistentStoragePerm(origin)); + + let updatePromise = promiseSitesUpdated(); + yield openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true }); + yield updatePromise; + yield openSettingsDialog(); + + let doc = gBrowser.selectedBrowser.contentDocument; + let frameDoc = null; + let saveBtn = null; + let cancelBtn = null; + let settingsDialogClosePromise = null; + + // Test the initial state + assertAllSitesListed(); + + // Test the "Cancel" button + settingsDialogClosePromise = promiseSettingsDialogClose(); + frameDoc = doc.getElementById("dialogFrame").contentDocument; + cancelBtn = frameDoc.getElementById("cancel"); + removeAllSitesOneByOne(); + assertAllSitesNotListed(); + cancelBtn.doCommand(); + yield settingsDialogClosePromise; + yield openSettingsDialog(); + assertAllSitesListed(); + + // Test the "Save Changes" button but cancelling save + let cancelPromise = promiseAlertDialogOpen("cancel"); + settingsDialogClosePromise = promiseSettingsDialogClose(); + frameDoc = doc.getElementById("dialogFrame").contentDocument; + saveBtn = frameDoc.getElementById("save"); + removeAllSitesOneByOne(); + assertAllSitesNotListed(); + saveBtn.doCommand(); + yield cancelPromise; + yield settingsDialogClosePromise; + yield openSettingsDialog(); + assertAllSitesListed(); + + // Test the "Save Changes" button and accepting save + let acceptPromise = promiseAlertDialogOpen("accept"); + settingsDialogClosePromise = promiseSettingsDialogClose(); + updatePromise = promiseSitesUpdated(); + frameDoc = doc.getElementById("dialogFrame").contentDocument; + saveBtn = frameDoc.getElementById("save"); + removeAllSitesOneByOne(); + assertAllSitesNotListed(); + saveBtn.doCommand(); + yield acceptPromise; + yield settingsDialogClosePromise; + yield updatePromise; + yield openSettingsDialog(); + assertAllSitesNotListed(); + + // Always clean up the fake origins + fakeOrigins.forEach(origin => removePersistentStoragePerm(origin)); + yield BrowserTestUtils.removeTab(gBrowser.selectedTab); + + function removeAllSitesOneByOne() { + frameDoc = doc.getElementById("dialogFrame").contentDocument; + let removeBtn = frameDoc.getElementById("removeSelected"); + let sitesList = frameDoc.getElementById("sitesList"); + let sites = sitesList.getElementsByTagName("richlistitem"); + for (let i = sites.length - 1; i >= 0; --i) { + sites[i].click(); + removeBtn.doCommand(); + } + } + + function assertAllSitesListed() { + frameDoc = doc.getElementById("dialogFrame").contentDocument; + let removeBtn = frameDoc.getElementById("removeSelected"); + let sitesList = frameDoc.getElementById("sitesList"); + let sites = sitesList.getElementsByTagName("richlistitem"); + is(sites.length, fakeOrigins.length, "Should list all sites"); + is(removeBtn.disabled, false, "Should enable the removeSelected button"); + } + + function assertAllSitesNotListed() { + frameDoc = doc.getElementById("dialogFrame").contentDocument; + let removeBtn = frameDoc.getElementById("removeSelected"); + let sitesList = frameDoc.getElementById("sitesList"); + let sites = sitesList.getElementsByTagName("richlistitem"); + is(sites.length, 0, "Should not list all sites"); + is(removeBtn.disabled, true, "Should disable the removeSelected button"); + } +}); + +// Test selecting and removing partial sites +add_task(function* () { + yield SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]}); + let fakeOrigins = [ + "https://news.foo.com/", + "https://mails.bar.com/", + "https://videos.xyz.com/", + "https://books.foo.com/", + "https://account.bar.com/", + "https://shopping.xyz.com/" + ]; + fakeOrigins.forEach(origin => addPersistentStoragePerm(origin)); + + let updatePromise = promiseSitesUpdated(); + yield openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true }); + yield updatePromise; + yield openSettingsDialog(); + + const removeDialogURL = "chrome://browser/content/preferences/siteDataRemoveSelected.xul"; + let doc = gBrowser.selectedBrowser.contentDocument; + let frameDoc = null; + let saveBtn = null; + let cancelBtn = null; + let removeDialogOpenPromise = null; + let settingsDialogClosePromise = null; + + // Test the initial state + assertSitesListed(fakeOrigins); + + // Test the "Cancel" button + settingsDialogClosePromise = promiseSettingsDialogClose(); + frameDoc = doc.getElementById("dialogFrame").contentDocument; + cancelBtn = frameDoc.getElementById("cancel"); + removeSelectedSite(fakeOrigins.slice(0, 4)); + assertSitesListed(fakeOrigins.slice(4)); + cancelBtn.doCommand(); + yield settingsDialogClosePromise; + yield openSettingsDialog(); + assertSitesListed(fakeOrigins); + + // Test the "Save Changes" button but canceling save + removeDialogOpenPromise = promiseWindowDialogOpen("cancel", removeDialogURL); + settingsDialogClosePromise = promiseSettingsDialogClose(); + frameDoc = doc.getElementById("dialogFrame").contentDocument; + saveBtn = frameDoc.getElementById("save"); + removeSelectedSite(fakeOrigins.slice(0, 4)); + assertSitesListed(fakeOrigins.slice(4)); + saveBtn.doCommand(); + yield removeDialogOpenPromise; + yield settingsDialogClosePromise; + yield openSettingsDialog(); + assertSitesListed(fakeOrigins); + + // Test the "Save Changes" button and accepting save + removeDialogOpenPromise = promiseWindowDialogOpen("accept", removeDialogURL); + settingsDialogClosePromise = promiseSettingsDialogClose(); + frameDoc = doc.getElementById("dialogFrame").contentDocument; + saveBtn = frameDoc.getElementById("save"); + removeSelectedSite(fakeOrigins.slice(0, 4)); + assertSitesListed(fakeOrigins.slice(4)); + saveBtn.doCommand(); + yield removeDialogOpenPromise; + yield settingsDialogClosePromise; + yield openSettingsDialog(); + assertSitesListed(fakeOrigins.slice(4)); + + // Always clean up the fake origins + fakeOrigins.forEach(origin => removePersistentStoragePerm(origin)); + yield BrowserTestUtils.removeTab(gBrowser.selectedTab); + + function removeSelectedSite(origins) { + frameDoc = doc.getElementById("dialogFrame").contentDocument; + let removeBtn = frameDoc.getElementById("removeSelected"); + let sitesList = frameDoc.getElementById("sitesList"); + origins.forEach(origin => { + let site = sitesList.querySelector(`richlistitem[data-origin="${origin}"]`); + if (site) { + site.click(); + removeBtn.doCommand(); + } else { + ok(false, `Should not select and remove inexisted site of ${origin}`); + } + }); + } + + function assertSitesListed(origins) { + frameDoc = doc.getElementById("dialogFrame").contentDocument; + let removeBtn = frameDoc.getElementById("removeSelected"); + let sitesList = frameDoc.getElementById("sitesList"); + let totalSitesNumber = sitesList.getElementsByTagName("richlistitem").length; + is(totalSitesNumber, origins.length, "Should list the right sites number"); + origins.forEach(origin => { + let site = sitesList.querySelector(`richlistitem[data-origin="${origin}"]`); + ok(!!site, `Should list the site of ${origin}`); + }); + is(removeBtn.disabled, false, "Should enable the removeSelected button"); + } +}); diff --git a/browser/components/preferences/in-content/tests/head.js b/browser/components/preferences/in-content/tests/head.js index 06e7fd1cfc53..e1bcd164b439 100644 --- a/browser/components/preferences/in-content/tests/head.js +++ b/browser/components/preferences/in-content/tests/head.js @@ -161,12 +161,12 @@ function waitForCondition(aConditionFn, aMaxTries = 50, aCheckInterval = 100) { }); } -function promiseAlertDialogOpen(buttonAction) { +function promiseWindowDialogOpen(buttonAction, url) { return new Promise(resolve => { Services.ww.registerNotification(function onOpen(subj, topic, data) { if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) { - subj.addEventListener("load", function() { - if (subj.document.documentURI == "chrome://global/content/commonDialog.xul") { + subj.addEventListener("load", function onLoad() { + if (subj.document.documentURI == url) { Services.ww.unregisterNotification(onOpen); let doc = subj.document.documentElement; doc.getButton(buttonAction).click(); @@ -177,3 +177,7 @@ function promiseAlertDialogOpen(buttonAction) { }); }); } + +function promiseAlertDialogOpen(buttonAction) { + return promiseWindowDialogOpen(buttonAction, "chrome://global/content/commonDialog.xul"); +} diff --git a/browser/components/preferences/jar.mn b/browser/components/preferences/jar.mn index fdf5bd0880b1..4436d05eb2f1 100644 --- a/browser/components/preferences/jar.mn +++ b/browser/components/preferences/jar.mn @@ -30,6 +30,8 @@ browser.jar: content/browser/preferences/siteDataSettings.xul content/browser/preferences/siteDataSettings.js content/browser/preferences/siteDataSettings.css +* content/browser/preferences/siteDataRemoveSelected.xul + content/browser/preferences/siteDataRemoveSelected.js content/browser/preferences/siteListItem.xml content/browser/preferences/translation.xul content/browser/preferences/translation.js diff --git a/browser/components/preferences/moz.build b/browser/components/preferences/moz.build index c45daebe1e52..49efefa37306 100644 --- a/browser/components/preferences/moz.build +++ b/browser/components/preferences/moz.build @@ -19,7 +19,7 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'gtk2', 'gtk3', 'cocoa'): JAR_MANIFESTS += ['jar.mn'] EXTRA_JS_MODULES += [ - 'SiteDataManager.jsm', + 'SiteDataManager.jsm' ] with Files('**'): diff --git a/browser/components/preferences/siteDataRemoveSelected.js b/browser/components/preferences/siteDataRemoveSelected.js new file mode 100644 index 000000000000..8aac90b8c324 --- /dev/null +++ b/browser/components/preferences/siteDataRemoveSelected.js @@ -0,0 +1,197 @@ +/* -*- 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 { utils: Cu } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +"use strict"; + +let gSiteDataRemoveSelected = { + + _tree: null, + + init() { + // Organize items for the tree from the argument + let hostsTable = window.arguments[0].hostsTable; + let visibleItems = []; + let itemsTable = new Map(); + for (let [ baseDomain, hosts ] of hostsTable) { + // In the beginning, only display base domains in the topmost level. + visibleItems.push({ + level: 0, + opened: false, + host: baseDomain + }); + // Other hosts are in the second level. + let items = hosts.map(host => { + return { host, level: 1 }; + }); + items.sort(sortByHost); + itemsTable.set(baseDomain, items); + } + visibleItems.sort(sortByHost); + this._view.itemsTable = itemsTable; + this._view.visibleItems = visibleItems; + this._tree = document.getElementById("sitesTree"); + this._tree.view = this._view; + + function sortByHost(a, b) { + let aHost = a.host.toLowerCase(); + let bHost = b.host.toLowerCase(); + return aHost.localeCompare(bHost); + } + }, + + ondialogaccept() { + window.arguments[0].allowed = true; + }, + + ondialogcancel() { + window.arguments[0].allowed = false; + }, + + _view: { + _selection: null, + + itemsTable: null, + + visibleItems: null, + + get rowCount() { + return this.visibleItems.length; + }, + + getCellText(index, column) { + let item = this.visibleItems[index]; + return item ? item.host : ""; + }, + + isContainer(index) { + let item = this.visibleItems[index]; + if (item && item.level === 0) { + return true; + } + return false; + }, + + isContainerEmpty() { + return false; + }, + + isContainerOpen(index) { + let item = this.visibleItems[index]; + if (item && item.level === 0) { + return item.opened; + } + return false; + }, + + getLevel(index) { + let item = this.visibleItems[index]; + return item ? item.level : 0; + }, + + hasNextSibling(index, afterIndex) { + let item = this.visibleItems[index]; + if (item) { + let thisLV = this.getLevel(index); + for (let i = afterIndex + 1; i < this.rowCount; ++i) { + let nextLV = this.getLevel(i); + if (nextLV == thisLV) { + return true; + } + if (nextLV < thisLV) { + break; + } + } + } + return false; + }, + + getParentIndex(index) { + if (!this.isContainer(index)) { + for (let i = index - 1; i >= 0; --i) { + if (this.isContainer(i)) { + return i; + } + } + } + return -1; + }, + + toggleOpenState(index) { + let item = this.visibleItems[index]; + if (!this.isContainer(index)) { + return; + } + + if (item.opened) { + item.opened = false; + + let deleteCount = 0; + for (let i = index + 1; i < this.visibleItems.length; ++i) { + if (!this.isContainer(i)) { + ++deleteCount; + } else { + break; + } + } + + if (deleteCount) { + this.visibleItems.splice(index + 1, deleteCount); + this.treeBox.rowCountChanged(index + 1, -deleteCount); + } + } else { + item.opened = true; + + let childItems = this.itemsTable.get(item.host); + for (let i = 0; i < childItems.length; ++i) { + this.visibleItems.splice(index + i + 1, 0, childItems[i]); + } + this.treeBox.rowCountChanged(index + 1, childItems.length); + } + this.treeBox.invalidateRow(index); + }, + + get selection() { + return this._selection; + }, + set selection(v) { + this._selection = v; + return v; + }, + setTree(treeBox) { + this.treeBox = treeBox; + }, + isSeparator(index) { + return false; + }, + isSorted(index) { + return false; + }, + canDrop() { + return false; + }, + drop() {}, + getRowProperties() {}, + getCellProperties() {}, + getColumnProperties() {}, + hasPreviousSibling(index) {}, + getImageSrc() {}, + getProgressMode() {}, + getCellValue() {}, + cycleHeader() {}, + selectionChanged() {}, + cycleCell() {}, + isEditable() {}, + isSelectable() {}, + setCellValue() {}, + setCellText() {}, + performAction() {}, + performActionOnRow() {}, + performActionOnCell() {} + } +}; diff --git a/browser/components/preferences/siteDataRemoveSelected.xul b/browser/components/preferences/siteDataRemoveSelected.xul new file mode 100644 index 000000000000..a3996560b15e --- /dev/null +++ b/browser/components/preferences/siteDataRemoveSelected.xul @@ -0,0 +1,58 @@ + + + + + + + + + + + + +