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 draft
authorThomas Wisniewski <wisniewskit@gmail.com>
Fri, 05 May 2017 16:21:32 -0400
changeset 573478 7c7011f7cf7e73b7668a4b363d6f0d097332ded9
parent 573359 23fe0b76a018a5077a0f7234cff91c41e4b6af64
child 627312 f130ee59c44e8fa9ea52a02061dd57bec97fbdf4
push id57404
push userwisniewskit@gmail.com
push dateFri, 05 May 2017 20:26:39 +0000
reviewersjanv
bugs1333050
milestone55.0a1
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
dom/quota/ActorsParent.cpp
dom/quota/PQuota.ipdl
dom/quota/QuotaManagerService.cpp
dom/quota/nsIQuotaManagerService.idl
dom/quota/test/unit/head.js
dom/quota/test/unit/test_clearStorages.js
dom/quota/test/unit/xpcshell.ini
--- 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]