Bug 1333050 - Add a new method QuotaManagerService.clearStorages(in jsval aOptions), which is a dict with an array of storageTypes (only indexedDB supported for now), optional array of principals, and optional 'since' cutoff timestamp; r?janv
MozReview-Commit-ID: IAfxiVj7Luk
--- a/dom/quota/ActorsParent.cpp
+++ b/dom/quota/ActorsParent.cpp
@@ -1271,20 +1271,16 @@ class ClearRequestBase
{
protected:
explicit ClearRequestBase(bool aExclusive)
: QuotaRequestBase(aExclusive)
{
AssertIsOnOwningThread();
}
- void
- DeleteFiles(QuotaManager* aQuotaManager,
- PersistenceType aPersistenceType);
-
nsresult
DoDirectoryWork(QuotaManager* aQuotaManager) override;
};
class ClearOriginOp final
: public ClearRequestBase
{
const ClearOriginParams mParams;
@@ -1323,16 +1319,42 @@ private:
nsresult
DoInitOnMainThread() override;
void
GetResponse(RequestResponse& aResponse) override;
};
+class ClearStoragesOp final
+ : public ClearRequestBase
+{
+ const ClearStoragesParams mParams;
+
+public:
+ explicit ClearStoragesOp(const RequestParams& aParams);
+
+ bool
+ Init(Quota* aQuota) override;
+
+private:
+ ~ClearStoragesOp()
+ { }
+
+ nsresult
+ TraverseRepository(QuotaManager* aQuotaManager,
+ PersistenceType aPersistenceType);
+
+ nsresult
+ DoDirectoryWork(QuotaManager* aQuotaManager) override;
+
+ void
+ GetResponse(RequestResponse& aResponse) override;
+};
+
class PersistRequestBase
: public QuotaRequestBase
{
const PrincipalInfo mPrincipalInfo;
protected:
nsCString mSuffix;
nsCString mGroup;
@@ -6432,16 +6454,20 @@ Quota::AllocPQuotaRequestParent(const Re
case RequestParams::TClearOriginParams:
actor = new ClearOriginOp(aParams);
break;
case RequestParams::TClearDataParams:
actor = new ClearDataOp(aParams);
break;
+ case RequestParams::TClearStoragesParams:
+ actor = new ClearStoragesOp(aParams);
+ break;
+
case RequestParams::TClearAllParams:
actor = new ResetOrClearOp(/* aClear */ true);
break;
case RequestParams::TResetAllParams:
actor = new ResetOrClearOp(/* aClear */ false);
break;
@@ -7258,19 +7284,21 @@ ResetOrClearOp::GetResponse(RequestRespo
AssertIsOnOwningThread();
if (mClear) {
aResponse = ClearAllResponse();
} else {
aResponse = ResetAllResponse();
}
}
-void
-ClearRequestBase::DeleteFiles(QuotaManager* aQuotaManager,
- PersistenceType aPersistenceType)
+static void
+DeleteStorageFiles(QuotaManager* aQuotaManager,
+ PersistenceType aPersistenceType,
+ OriginScope* aOriginScope,
+ int64_t aSince=-1)
{
AssertIsOnIOThread();
MOZ_ASSERT(aQuotaManager);
nsresult rv;
nsCOMPtr<nsIFile> directory =
do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
@@ -7284,25 +7312,28 @@ ClearRequestBase::DeleteFiles(QuotaManag
}
nsCOMPtr<nsISimpleEnumerator> entries;
if (NS_WARN_IF(NS_FAILED(
directory->GetDirectoryEntries(getter_AddRefs(entries)))) || !entries) {
return;
}
- OriginScope originScope = mOriginScope.Clone();
- if (originScope.IsOrigin()) {
- nsCString originSanitized(originScope.GetOrigin());
- SanitizeOriginString(originSanitized);
- originScope.SetOrigin(originSanitized);
- } else if (originScope.IsPrefix()) {
- nsCString prefixSanitized(originScope.GetPrefix());
- SanitizeOriginString(prefixSanitized);
- originScope.SetPrefix(prefixSanitized);
+ OriginScope originScope = aOriginScope ? aOriginScope->Clone()
+ : OriginScope::FromNull();
+ if (aOriginScope) {
+ if (originScope.IsOrigin()) {
+ nsCString originSanitized(originScope.GetOrigin());
+ SanitizeOriginString(originSanitized);
+ originScope.SetOrigin(originSanitized);
+ } else if (originScope.IsPrefix()) {
+ nsCString prefixSanitized(originScope.GetPrefix());
+ SanitizeOriginString(prefixSanitized);
+ originScope.SetPrefix(prefixSanitized);
+ }
}
bool hasMore;
while (NS_SUCCEEDED((rv = entries->HasMoreElements(&hasMore))) && hasMore) {
nsCOMPtr<nsISupports> entry;
rv = entries->GetNext(getter_AddRefs(entry));
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
@@ -7327,17 +7358,17 @@ ClearRequestBase::DeleteFiles(QuotaManag
// Unknown files during clearing are allowed. Just warn if we find them.
if (!IsOSMetadata(leafName)) {
UNKNOWN_FILE_WARNING(leafName);
}
continue;
}
// Skip the origin directory if it doesn't match the pattern.
- if (!originScope.MatchesOrigin(OriginScope::FromOrigin(
+ if (aOriginScope && !originScope.MatchesOrigin(OriginScope::FromOrigin(
NS_ConvertUTF16toUTF8(leafName)))) {
continue;
}
bool persistent = aPersistenceType == PERSISTENCE_TYPE_PERSISTENT;
int64_t timestamp;
nsCString suffix;
@@ -7350,16 +7381,22 @@ ClearRequestBase::DeleteFiles(QuotaManag
&persisted,
suffix,
group,
origin);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
+ // Don't delete the directory if it's timestamp is older
+ // than the desired timestamp.
+ if (aSince >= 0 && timestamp < aSince) {
+ continue;
+ }
+
for (uint32_t index = 0; index < 10; index++) {
// We can't guarantee that this will always succeed on Windows...
if (NS_SUCCEEDED((rv = file->Remove(true)))) {
break;
}
NS_WARNING("Failed to remove directory, retrying after a short delay.");
@@ -7371,33 +7408,32 @@ ClearRequestBase::DeleteFiles(QuotaManag
}
if (aPersistenceType != PERSISTENCE_TYPE_PERSISTENT) {
aQuotaManager->RemoveQuotaForOrigin(aPersistenceType, group, origin);
}
aQuotaManager->OriginClearCompleted(aPersistenceType, origin);
}
-
}
nsresult
ClearRequestBase::DoDirectoryWork(QuotaManager* aQuotaManager)
{
AssertIsOnIOThread();
PROFILER_LABEL("Quota", "OriginClearOp::DoDirectoryWork",
js::ProfileEntry::Category::OTHER);
if (mPersistenceType.IsNull()) {
for (const PersistenceType type : kAllPersistenceTypes) {
- DeleteFiles(aQuotaManager, type);
+ DeleteStorageFiles(aQuotaManager, type, &mOriginScope);
}
} else {
- DeleteFiles(aQuotaManager, mPersistenceType.Value());
+ DeleteStorageFiles(aQuotaManager, mPersistenceType.Value(), &mOriginScope);
}
return NS_OK;
}
ClearOriginOp::ClearOriginOp(const RequestParams& aParams)
: ClearRequestBase(/* aExclusive */ true)
, mParams(aParams)
@@ -7504,16 +7540,87 @@ ClearDataOp::DoInitOnMainThread()
void
ClearDataOp::GetResponse(RequestResponse& aResponse)
{
AssertIsOnOwningThread();
aResponse = ClearDataResponse();
}
+ClearStoragesOp::ClearStoragesOp(const RequestParams& aParams)
+ : ClearRequestBase(/* aExclusive */ true)
+ , mParams(aParams)
+{
+ MOZ_ASSERT(aParams.type() == RequestParams::TClearStoragesParams);
+}
+
+bool
+ClearStoragesOp::Init(Quota* aQuota)
+{
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aQuota);
+
+ mNeedsQuotaManagerInit = true;
+
+ return true;
+}
+
+nsresult
+ClearStoragesOp::TraverseRepository(QuotaManager* aQuotaManager,
+ PersistenceType aPersistenceType)
+{
+ // We only clear IDBs right now.
+ if (!mParams.clearIndexedDBs()) {
+ return NS_OK;
+ }
+
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aQuotaManager);
+
+ int64_t since = mParams.since();
+
+ if (mParams.clearAllOrigins()) {
+ DeleteStorageFiles(aQuotaManager, aPersistenceType, nullptr, since);
+ } else {
+ for(uint32_t count = mParams.origins().Length(), i = 0; i < count; i++) {
+ OriginScope scope = OriginScope::FromOrigin(mParams.origins()[i]);
+ DeleteStorageFiles(aQuotaManager, aPersistenceType, &scope, since);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+ClearStoragesOp::DoDirectoryWork(QuotaManager* aQuotaManager)
+{
+ AssertIsOnIOThread();
+
+ PROFILER_LABEL("Quota", "GetMetadataOp::DoDirectoryWork",
+ js::ProfileEntry::Category::OTHER);
+
+ nsresult rv;
+
+ for (const PersistenceType type : kAllPersistenceTypes) {
+ rv = TraverseRepository(aQuotaManager, type);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+void
+ClearStoragesOp::GetResponse(RequestResponse& aResponse)
+{
+ AssertIsOnOwningThread();
+
+ aResponse = ClearDataResponse();
+}
+
PersistRequestBase::PersistRequestBase(const PrincipalInfo& aPrincipalInfo)
: QuotaRequestBase(/* aExclusive */ false)
, mPrincipalInfo(aPrincipalInfo)
{
AssertIsOnOwningThread();
}
bool
--- a/dom/quota/PQuota.ipdl
+++ b/dom/quota/PQuota.ipdl
@@ -52,16 +52,24 @@ struct ClearOriginParams
bool clearAll;
};
struct ClearDataParams
{
nsString pattern;
};
+struct ClearStoragesParams
+{
+ bool clearIndexedDBs;
+ bool clearAllOrigins;
+ nsCString[] origins;
+ int64_t since;
+};
+
struct ClearAllParams
{
};
struct ResetAllParams
{
};
@@ -76,16 +84,17 @@ struct PersistParams
};
union RequestParams
{
InitParams;
InitOriginParams;
ClearOriginParams;
ClearDataParams;
+ ClearStoragesParams;
ClearAllParams;
ResetAllParams;
PersistedParams;
PersistParams;
};
protocol PQuota
{
--- a/dom/quota/QuotaManagerService.cpp
+++ b/dom/quota/QuotaManagerService.cpp
@@ -662,16 +662,152 @@ QuotaManagerService::Clear(nsIQuotaReque
return rv;
}
request.forget(_retval);
return NS_OK;
}
NS_IMETHODIMP
+QuotaManagerService::ClearStorages(JS::Handle<JS::Value> aOptions,
+ nsIQuotaRequest** _retval)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!aOptions.isObject() || aOptions.isNullOrUndefined()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AutoJSAPI jsapi;
+ if (!jsapi.Init(&aOptions.toObject())) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ JSContext* cx = jsapi.cx();
+ if (NS_WARN_IF(!cx)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ClearStoragesParams params;
+
+ Maybe<JS::Rooted<JSObject *> > options;
+ options.emplace(cx, &aOptions.toObject());
+
+ JS::RootedValue storageTypes(cx);
+ if (JS_GetProperty(cx, *options, "storageTypes", &storageTypes) &&
+ storageTypes.isObject() && !storageTypes.isNullOrUndefined()) {
+ JS::ForOfIterator iter(cx);
+ if (!iter.init(storageTypes, JS::ForOfIterator::AllowNonIterable) ||
+ !iter.valueIsIterable()) {
+ ThrowErrorMessage(cx, MSG_NOT_SEQUENCE, "storageTypes of clearStorages");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ JS::Rooted<JS::Value> iterItem(cx);
+ while (true) {
+ bool done;
+ if (!iter.next(&iterItem, &done)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (done) {
+ break;
+ }
+
+ if (iterItem.isString()) {
+ bool match = false;
+ JS::RootedString str(cx, JS::ToString(cx, iterItem));
+ if (!JS_StringEqualsAscii(cx, str, "indexedDB", &match)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (match) {
+ params.clearIndexedDBs() = true;
+ } else {
+ char* bytes = JS_EncodeStringToUTF8(cx, str);
+ ThrowErrorMessage(cx, MSG_DOES_NOT_IMPLEMENT_INTERFACE,
+ "clearStorages", bytes);
+ JS_free(cx, bytes);
+ return NS_ERROR_INVALID_ARG;
+ }
+ } else {
+ ThrowErrorMessage(cx, MSG_INVALID_ENUM_VALUE, "clearStorages",
+ InformalValueTypeName(iterItem), "indexedDB");
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+ }
+
+ JS::RootedValue principals(cx);
+ if (JS_GetProperty(cx, *options, "principals", &principals) &&
+ principals.isObject() && !principals.isNullOrUndefined()) {
+ JS::ForOfIterator iter(cx);
+ if (!iter.init(principals, JS::ForOfIterator::AllowNonIterable) ||
+ !iter.valueIsIterable()) {
+ ThrowErrorMessage(cx, MSG_NOT_SEQUENCE, "principals of clearStorages");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsresult rv;
+ JS::Rooted<JS::Value> iterItem(cx);
+ while (true) {
+ bool done;
+ if (!iter.next(&iterItem, &done)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (done) {
+ break;
+ }
+
+ if (iterItem.isObject() && !iterItem.isNullOrUndefined()) {
+ JS::RootedObject obj(cx, &iterItem.toObject());
+ RefPtr<nsIPrincipal> principal;
+
+ if (NS_FAILED(UnwrapArg<nsIPrincipal>(cx, obj,
+ getter_AddRefs(principal)))) {
+ ThrowErrorMessage(cx, MSG_DOES_NOT_IMPLEMENT_INTERFACE,
+ "Argument of clearStorages", "Principal");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsCString origin;
+ rv = QuotaManager::GetInfoFromPrincipal(principal, nullptr, nullptr,
+ &origin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ params.origins().AppendElement(origin);
+ } else {
+ ThrowErrorMessage(cx, MSG_INVALID_ENUM_VALUE, "clearStorages",
+ InformalValueTypeName(iterItem), "nsIPrincipal");
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+ } else {
+ params.clearAllOrigins() = true;
+ }
+
+ JS::RootedValue since(cx);
+ if (JS_GetProperty(cx, *options, "since", &since) && since.isNumber()) {
+ params.since() = int64_t(since.toNumber());
+ } else {
+ params.since() = -1;
+ }
+
+ RefPtr<Request> request = new Request();
+ nsAutoPtr<PendingRequestInfo> info(new RequestInfo(request, params));
+
+ nsresult rv = InitiateRequest(info);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ request.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
QuotaManagerService::ClearStoragesForPrincipal(nsIPrincipal* aPrincipal,
const nsACString& aPersistenceType,
bool aClearAll,
nsIQuotaRequest** _retval)
{
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aPrincipal);
--- a/dom/quota/nsIQuotaManagerService.idl
+++ b/dom/quota/nsIQuotaManagerService.idl
@@ -82,16 +82,29 @@ interface nsIQuotaManagerService : nsISu
*
* If the dom.quotaManager.testing preference is not true the call will be
* a no-op.
*/
[must_use] nsIQuotaRequest
clear();
/**
+ * Removes all storages as defined by the given options.
+ *
+ * @param aOptions
+ * A jsval in the format: {
+ * storages: ["indexedDB"],
+ * principals: [Principal],
+ * since: int64_t
+ * }
+ */
+ [must_use] nsIQuotaRequest
+ clearStorages(in jsval aOptions);
+
+ /**
* Removes all storages stored for the given principal. The files may not be
* deleted immediately depending on prohibitive concurrent operations.
*
* @param aPrincipal
* A principal for the origin whose storages are to be cleared.
* @param aPersistenceType
* An optional string that tells what persistence type of storages
* will be cleared.
--- a/dom/quota/test/unit/head.js
+++ b/dom/quota/test/unit/head.js
@@ -112,16 +112,24 @@ function initChromeOrigin(persistence, c
function clear(callback)
{
let request = SpecialPowers._getQuotaManager().clear();
request.callback = callback;
return request;
}
+function clearStorages(opts, callback)
+{
+ let request = SpecialPowers._getQuotaManager().clearStorages(opts);
+ request.callback = callback;
+
+ return request;
+}
+
function clearOrigin(principal, persistence, callback)
{
let request =
SpecialPowers._getQuotaManager().clearStoragesForPrincipal(principal,
persistence);
request.callback = callback;
return request;
@@ -184,17 +192,23 @@ function installPackage(packageName)
entryNames.sort();
for (let entryName of entryNames) {
let zipentry = zipReader.getEntry(entryName);
let file = getRelativeFile(entryName);
if (zipentry.isDirectory) {
- file.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+ try {
+ file.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
+ throw e;
+ }
+ }
} else {
let istream = zipReader.getInputStream(entryName);
var ostream = Cc["@mozilla.org/network/file-output-stream;1"]
.createInstance(Ci.nsIFileOutputStream);
ostream.init(file, -1, parseInt("0644", 8), 0);
let bostream = Cc['@mozilla.org/network/buffered-output-stream;1']
new file mode 100644
--- /dev/null
+++ b/dom/quota/test/unit/test_clearStorages.js
@@ -0,0 +1,70 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var testGenerator = testSteps();
+
+function checkIDBExists(host) {
+ let dir = getRelativeFile("storage/default/" + host.replace("://", "+++"));
+ return dir.exists();
+}
+
+function* testSteps()
+{
+ info("Clearing");
+
+ clear(continueToNextStepSync);
+ yield undefined;
+
+ info("Installing package");
+ // The profile contains IndexedDB databases placed across the repositories.
+ // The file create_db.js in the package was run locally, specifically it was
+ // temporarily added to xpcshell.ini and then executed:
+ // mach xpcshell-test --interactive dom/quota/test/unit/create_db.js
+ installPackage("getUsage_profile");
+
+ info("Testing clearStorages(indexedDBs for principals since)");
+ let request = clearStorages({
+ storageTypes: ["indexedDB"],
+ principals: [getPrincipal("http://localhost"),
+ getPrincipal("http://example.com")],
+ since: 1489668514043410
+ }, continueToNextStepSync);
+ yield undefined;
+ ok(request.resultCode == NS_OK, "Clearing succeeded");
+ ok(checkIDBExists("http://localhost"), "Older IDB was not cleared");
+ ok(checkIDBExists("http://www.mozilla.org"), "IDB not in principals list was not cleared");
+ ok(!checkIDBExists("http://example.com"), "Newer IDB was cleared");
+
+ info("Re-installing package");
+ installPackage("getUsage_profile");
+
+ info("Testing clearStorages(all indexedDBs since)");
+ request = clearStorages({
+ storageTypes: ["indexedDB"],
+ since: 1489668514043410
+ }, continueToNextStepSync);
+ yield undefined;
+ ok(request.resultCode == NS_OK, "Clearing succeeded");
+ ok(checkIDBExists("http://localhost"), "Older IDB was not cleared");
+ ok(!checkIDBExists("http://www.mozilla.org"), "IDB at cutoff was cleared");
+ ok(!checkIDBExists("http://example.com"), "Newer IDB was cleared");
+
+ info("Re-installing package");
+ installPackage("getUsage_profile");
+
+ info("Testing clearStorages(all indexedDBs)");
+ request = clearStorages({
+ storageTypes: ["indexedDB"]
+ }, continueToNextStepSync);
+ yield undefined;
+ ok(request.resultCode == NS_OK, "Clearing succeeded");
+
+ ok(!checkIDBExists("http://example.com"), "IDB was cleared");
+ ok(!checkIDBExists("http://www.mozilla.org"), "IDB was cleared");
+ ok(!checkIDBExists("http://localhost"), "IDB was cleared");
+
+ finishTest();
+}
+
--- a/dom/quota/test/unit/xpcshell.ini
+++ b/dom/quota/test/unit/xpcshell.ini
@@ -23,11 +23,12 @@ skip-if = release_or_beta
[test_defaultStorageUpgrade.js]
[test_getUsage.js]
[test_idbSubdirUpgrade.js]
[test_morgueCleanup.js]
[test_obsoleteOriginAttributesUpgrade.js]
[test_originAttributesUpgrade.js]
[test_persist.js]
[test_removeAppsUpgrade.js]
+[test_clearStorages.js]
[test_storagePersistentUpgrade.js]
[test_tempMetadataCleanup.js]
[test_unknownFiles.js]