Bug 1388428 - Extend browsingData to restrict removing localStorage to a given list of hostnames; r=bsilverberg,janv
MozReview-Commit-ID: 5TYPjyZusfd
--- a/browser/components/extensions/ext-browsingData.js
+++ b/browser/components/extensions/ext-browsingData.js
@@ -79,17 +79,22 @@ const clearFormData = options => {
return sanitizer.items.formdata.clear(makeRange(options));
};
const clearHistory = options => {
return sanitizer.items.history.clear(makeRange(options));
};
const clearLocalStorage = async function(options) {
- Services.obs.notifyObservers(null, "extension:purge-localStorage");
+ if (options.since) {
+ return Promise.reject(
+ {message: "Firefox does not support clearing localStorage with 'since'."});
+ }
+ let hosts = (options.hostnames || []).join("^");
+ Services.obs.notifyObservers(null, "extension:purge-localStorage", hosts);
};
const clearPasswords = async function(options) {
let loginManager = Services.logins;
let yieldCounter = 0;
if (options.since) {
// Iterate through the logins and delete any updated after our cutoff.
--- a/browser/components/extensions/schemas/browsing_data.json
+++ b/browser/components/extensions/schemas/browsing_data.json
@@ -31,17 +31,17 @@
"$ref": "extensionTypes.Date",
"optional": true,
"description": "Remove data accumulated on or after this date, represented in milliseconds since the epoch (accessible via the <code>getTime</code> method of the JavaScript <code>Date</code> object). If absent, defaults to 0 (which would remove all browsing data)."
},
"hostnames": {
"type": "array",
"items": {"type": "string", "format": "hostname"},
"optional": true,
- "description": "Only remove data associated with these hostnames (only applies to cookies)."
+ "description": "Only remove data associated with these hostnames (only applies to cookies and localStorage)."
},
"originTypes": {
"type": "object",
"optional": true,
"description": "An object whose properties specify which origin types ought to be cleared. If this object isn't specified, it defaults to clearing only \"unprotected\" origins. Please ensure that you <em>really</em> want to remove application data before adding 'protectedWeb' or 'extensions'.",
"properties": {
"unprotectedWeb": {
"type": "boolean",
--- a/browser/components/extensions/test/browser/browser_ext_browsingData_localStorage.js
+++ b/browser/components/extensions/test/browser/browser_ext_browsingData_localStorage.js
@@ -34,16 +34,27 @@ add_task(async function testLocalStorage
function sendMessageToTabs(tabs, message) {
return Promise.all(
tabs.map(tab => { return browser.tabs.sendMessage(tab.id, message); }));
}
let tabs = await openTabs();
+ browser.test.assertRejects(
+ browser.browsingData.removeLocalStorage({since: Date.now()}),
+ "Firefox does not support clearing localStorage with 'since'.",
+ "Expected error received when using unimplemented parameter 'since'."
+ );
+
+ await sendMessageToTabs(tabs, "resetLocalStorage");
+ await browser.browsingData.removeLocalStorage({hostnames: ["example.com"]});
+ await browser.tabs.sendMessage(tabs[0].id, "checkLocalStorageCleared");
+ await browser.tabs.sendMessage(tabs[1].id, "checkLocalStorageSet");
+
await sendMessageToTabs(tabs, "resetLocalStorage");
await sendMessageToTabs(tabs, "checkLocalStorageSet");
await browser.browsingData.removeLocalStorage({});
await sendMessageToTabs(tabs, "checkLocalStorageCleared");
await sendMessageToTabs(tabs, "resetLocalStorage");
await sendMessageToTabs(tabs, "checkLocalStorageSet");
await browser.browsingData.remove({}, {localStorage: true});
--- a/dom/storage/LocalStorageManager.cpp
+++ b/dom/storage/LocalStorageManager.cpp
@@ -342,37 +342,50 @@ NS_IMETHODIMP
LocalStorageManager::GetLocalStorageForPrincipal(nsIPrincipal* aPrincipal,
const nsAString& aDocumentURI,
bool aPrivate,
nsIDOMStorage** aRetval)
{
return CreateStorage(nullptr, aPrincipal, aDocumentURI, aPrivate, aRetval);
}
+inline bool
+OriginScopesContain(const nsACString& aString, const nsCSubstringSplitter& aScopes)
+{
+ for (const nsACString& scope : aScopes) {
+ if (StringBeginsWith(aString, scope)) {
+ return true;
+ }
+ }
+ return false;
+}
+
void
LocalStorageManager::ClearCaches(uint32_t aUnloadFlags,
const OriginAttributesPattern& aPattern,
- const nsACString& aOriginScope)
+ const nsACString& aOriginScopes)
{
+ nsCSubstringSplitter scopes = aOriginScopes.Split('^');
+
for (auto iter1 = mCaches.Iter(); !iter1.Done(); iter1.Next()) {
OriginAttributes oa;
DebugOnly<bool> rv = oa.PopulateFromSuffix(iter1.Key());
MOZ_ASSERT(rv);
if (!aPattern.Matches(oa)) {
// This table doesn't match the given origin attributes pattern
continue;
}
CacheOriginHashtable* table = iter1.Data();
for (auto iter2 = table->Iter(); !iter2.Done(); iter2.Next()) {
LocalStorageCache* cache = iter2.Get()->cache();
- if (aOriginScope.IsEmpty() ||
- StringBeginsWith(cache->OriginNoSuffix(), aOriginScope)) {
+ if (aOriginScopes.IsEmpty() ||
+ OriginScopesContain(cache->OriginNoSuffix(), scopes)) {
cache->UnloadItems(aUnloadFlags);
}
}
}
}
nsresult
LocalStorageManager::Observe(const char* aTopic,
@@ -383,17 +396,17 @@ LocalStorageManager::Observe(const char*
if (!pattern.Init(aOriginAttributesPattern)) {
NS_ERROR("Cannot parse origin attributes pattern");
return NS_ERROR_FAILURE;
}
// Clear everything, caches + database
if (!strcmp(aTopic, "cookie-cleared") ||
!strcmp(aTopic, "extension:purge-localStorage-caches")) {
- ClearCaches(LocalStorageCache::kUnloadComplete, pattern, EmptyCString());
+ ClearCaches(LocalStorageCache::kUnloadComplete, pattern, aOriginScope);
return NS_OK;
}
// Clear from caches everything that has been stored
// while in session-only mode
if (!strcmp(aTopic, "session-only-cleared")) {
ClearCaches(LocalStorageCache::kUnloadSession, pattern, aOriginScope);
return NS_OK;
--- a/dom/storage/StorageObserver.cpp
+++ b/dom/storage/StorageObserver.cpp
@@ -140,16 +140,39 @@ StorageObserver::Notify(const char* aTop
const nsACString& aOriginScope)
{
for (uint32_t i = 0; i < mSinks.Length(); ++i) {
StorageObserverSink* sink = mSinks[i];
sink->Observe(aTopic, aOriginAttributesPattern, aOriginScope);
}
}
+nsresult
+GetOriginScopeForDomain(const nsACString& aDomain, nsACString& aOriginScope)
+{
+ // Convert the domain name to the ACE format
+ nsresult rv;
+ nsAutoCString aceDomain;
+ nsCOMPtr<nsIIDNService> converter = do_GetService(NS_IDNSERVICE_CONTRACTID);
+ if (converter) {
+ rv = converter->ConvertUTF8toACE(aDomain, aceDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // In case the IDN service is not available, this is the best we can come
+ // up with!
+ rv = NS_EscapeURL(aDomain,
+ esc_OnlyNonASCII | esc_AlwaysCopy,
+ aceDomain,
+ fallible);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return CreateReversedDomain(aceDomain, aOriginScope);
+}
+
NS_IMETHODIMP
StorageObserver::Observe(nsISupports* aSubject,
const char* aTopic,
const char16_t* aData)
{
nsresult rv;
// Start the thread that opens the database.
@@ -249,47 +272,54 @@ StorageObserver::Observe(nsISupports* aS
Notify("session-only-cleared", NS_ConvertUTF8toUTF16(originSuffix),
originScope);
return NS_OK;
}
if (!strcmp(aTopic, "extension:purge-localStorage")) {
- StorageDBBridge* db = LocalStorageCache::StartDatabase();
- NS_ENSURE_TRUE(db, NS_ERROR_FAILURE);
+ nsCString data = NS_ConvertUTF16toUTF8(aData);
+ if (data.IsEmpty()) {
+ StorageDBBridge* db = LocalStorageCache::StartDatabase();
+ NS_ENSURE_TRUE(db, NS_ERROR_FAILURE);
+
+ db->AsyncClearAll();
+ } else {
+ nsTArray<nsCString> originScopes;
+ nsAutoCString originScope;
+ for (const nsACString& domain : data.Split('^')) {
+ rv = GetOriginScopeForDomain(domain, originScope);
+ NS_ENSURE_SUCCESS(rv, rv);
+ originScopes.AppendElement(originScope);
+ }
- db->AsyncClearAll();
+ StorageDBBridge* db = LocalStorageCache::StartDatabase();
+ NS_ENSURE_TRUE(db, NS_ERROR_FAILURE);
- Notify("extension:purge-localStorage-caches");
+ data = EmptyCString();
+ for (uint32_t i=0; i<originScopes.Length(); ++i) {
+ if (i > 0) {
+ data += "^";
+ }
+ db->AsyncClearMatchingOrigin(originScopes[i]);
+ data += originScopes[i];
+ }
+ }
+
+ Notify("extension:purge-localStorage-caches", EmptyString(), data);
return NS_OK;
}
// Clear everything (including so and pb data) from caches and database
// for the gived domain and subdomains.
if (!strcmp(aTopic, "browser:purge-domain-data")) {
- // Convert the domain name to the ACE format
- nsAutoCString aceDomain;
- nsCOMPtr<nsIIDNService> converter = do_GetService(NS_IDNSERVICE_CONTRACTID);
- if (converter) {
- rv = converter->ConvertUTF8toACE(NS_ConvertUTF16toUTF8(aData), aceDomain);
- NS_ENSURE_SUCCESS(rv, rv);
- } else {
- // In case the IDN service is not available, this is the best we can come
- // up with!
- rv = NS_EscapeURL(NS_ConvertUTF16toUTF8(aData),
- esc_OnlyNonASCII | esc_AlwaysCopy,
- aceDomain,
- fallible);
- NS_ENSURE_SUCCESS(rv, rv);
- }
-
nsAutoCString originScope;
- rv = CreateReversedDomain(aceDomain, originScope);
+ rv = GetOriginScopeForDomain(NS_ConvertUTF16toUTF8(aData), originScope);
NS_ENSURE_SUCCESS(rv, rv);
StorageDBBridge* db = LocalStorageCache::StartDatabase();
NS_ENSURE_TRUE(db, NS_ERROR_FAILURE);
db->AsyncClearMatchingOrigin(originScope);
Notify("domain-data-cleared", EmptyString(), originScope);