Browse Source

feat: `Session#clearData` API (#40983)

* WIP: Session.clearBrowsingData API

* impl API method

* clean up

* tidy types and comments

* add docs

* add barebones test

* forgot a `#` :(

* tidy: address review comments

* use format macro for cross-platform build

* add another test

* amend docs to disambiguate

* Rename to `clearData`
Calvin 1 year ago
parent
commit
12d7a8ff66

+ 20 - 0
docs/api/session.md

@@ -1424,6 +1424,26 @@ is emitted.
 Returns `string | null` - The absolute file system path where data for this
 session is persisted on disk.  For in memory sessions this returns `null`.
 
+#### `ses.clearData()`
+
+Returns `Promise<void>` - resolves when all data has been cleared.
+
+This method clears many different types of data, inlcuding:
+
+* Cache
+* Cookies
+* Downloads
+* IndexedDB
+* Local Storage
+* Service Workers
+* And more...
+
+This method clears more types of data and is more thourough than the
+`clearStorageData` method, however it is currently less configurable than that
+method.
+
+For more information, refer to Chromium's [`BrowsingDataRemover` interface](https://source.chromium.org/chromium/chromium/src/+/main:content/public/browser/browsing_data_remover.h).
+
 ### Instance Properties
 
 The following properties are available on instances of `Session`:

+ 53 - 0
shell/browser/api/electron_api_session.cc

@@ -16,6 +16,7 @@
 #include "base/files/file_path.h"
 #include "base/files/file_util.h"
 #include "base/memory/raw_ptr.h"
+#include "base/scoped_observation.h"
 #include "base/strings/string_util.h"
 #include "base/strings/stringprintf.h"
 #include "base/uuid.h"
@@ -32,6 +33,7 @@
 #include "content/browser/code_cache/generated_code_cache_context.h"  // nogncheck
 #include "content/public/browser/browser_task_traits.h"
 #include "content/public/browser/browser_thread.h"
+#include "content/public/browser/browsing_data_remover.h"
 #include "content/public/browser/download_item_utils.h"
 #include "content/public/browser/download_manager_delegate.h"
 #include "content/public/browser/network_service_instance.h"
@@ -77,6 +79,7 @@
 #include "shell/common/gin_converters/value_converter.h"
 #include "shell/common/gin_helper/dictionary.h"
 #include "shell/common/gin_helper/object_template_builder.h"
+#include "shell/common/gin_helper/promise.h"
 #include "shell/common/node_includes.h"
 #include "shell/common/options_switches.h"
 #include "shell/common/process_util.h"
@@ -149,6 +152,40 @@ uint32_t GetQuotaMask(const std::vector<std::string>& quota_types) {
   return quota_mask;
 }
 
+constexpr content::BrowsingDataRemover::DataType kClearDataTypeAll = ~0ULL;
+constexpr content::BrowsingDataRemover::OriginType kClearOriginTypeAll =
+    content::BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB |
+    content::BrowsingDataRemover::ORIGIN_TYPE_PROTECTED_WEB;
+
+// Observes the BrowsingDataRemover that backs the `clearData` method and
+// fulfills that API's promise once it's done. This type manages its own
+// lifetime, deleting itself once it's done.
+class ClearDataObserver : public content::BrowsingDataRemover::Observer {
+ public:
+  ClearDataObserver(gin_helper::Promise<void> promise,
+                    content::BrowsingDataRemover* remover)
+      : promise_(std::move(promise)) {
+    observation_.Observe(remover);
+  }
+
+  void OnBrowsingDataRemoverDone(
+      content::BrowsingDataRemover::DataType failed_data_types) override {
+    if (failed_data_types == 0ULL) {
+      promise_.Resolve();
+    } else {
+      promise_.RejectWithErrorMessage(base::StringPrintf(
+          "Failed to clear browsing data (%" PRIu64 ")", failed_data_types));
+    }
+    delete this;
+  }
+
+ private:
+  gin_helper::Promise<void> promise_;
+  base::ScopedObservation<content::BrowsingDataRemover,
+                          content::BrowsingDataRemover::Observer>
+      observation_{this};
+};
+
 base::Value::Dict createProxyConfig(ProxyPrefs::ProxyMode proxy_mode,
                                     std::string const& pac_url,
                                     std::string const& proxy_server,
@@ -1101,6 +1138,21 @@ v8::Local<v8::Promise> Session::ClearCodeCaches(
   return handle;
 }
 
+v8::Local<v8::Promise> Session::ClearData(gin::Arguments* args) {
+  auto* isolate = JavascriptEnvironment::GetIsolate();
+  gin_helper::Promise<void> promise(isolate);
+  v8::Local<v8::Promise> promise_handle = promise.GetHandle();
+
+  content::BrowsingDataRemover* remover =
+      browser_context_->GetBrowsingDataRemover();
+
+  auto* observer = new ClearDataObserver(std::move(promise), remover);
+  remover->RemoveAndReply(base::Time::Min(), base::Time::Max(),
+                          kClearDataTypeAll, kClearOriginTypeAll, observer);
+
+  return promise_handle;
+}
+
 #if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
 base::Value Session::GetSpellCheckerLanguages() {
   return browser_context_->prefs()
@@ -1370,6 +1422,7 @@ void Session::FillObjectTemplate(v8::Isolate* isolate,
       .SetMethod("getStoragePath", &Session::GetPath)
       .SetMethod("setCodeCachePath", &Session::SetCodeCachePath)
       .SetMethod("clearCodeCaches", &Session::ClearCodeCaches)
+      .SetMethod("clearData", &Session::ClearData)
       .SetProperty("cookies", &Session::Cookies)
       .SetProperty("netLog", &Session::NetLog)
       .SetProperty("protocol", &Session::Protocol)

+ 1 - 0
shell/browser/api/electron_api_session.h

@@ -147,6 +147,7 @@ class Session : public gin::Wrappable<Session>,
   v8::Local<v8::Value> GetPath(v8::Isolate* isolate);
   void SetCodeCachePath(gin::Arguments* args);
   v8::Local<v8::Promise> ClearCodeCaches(const gin_helper::Dictionary& options);
+  v8::Local<v8::Promise> ClearData(gin::Arguments* args);
 #if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
   base::Value GetSpellCheckerLanguages();
   void SetSpellCheckerLanguages(gin_helper::ErrorThrower thrower,

+ 32 - 0
spec/api-session-spec.ts

@@ -1607,4 +1607,36 @@ describe('session module', () => {
       await expect(request()).to.be.rejectedWith(/ERR_SSL_VERSION_OR_CIPHER_MISMATCH/);
     });
   });
+
+  describe('ses.clearData()', () => {
+    afterEach(closeAllWindows);
+
+    // NOTE: This API clears more than localStorage, but localStorage is a
+    // convenient test target for this API
+    it('clears localstorage data', async () => {
+      const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } });
+      await w.loadFile(path.join(fixtures, 'api', 'localstorage.html'));
+
+      expect(await w.webContents.executeJavaScript('localStorage.length')).to.be.greaterThan(0);
+
+      await w.webContents.session.clearData();
+
+      expect(await w.webContents.executeJavaScript('localStorage.length')).to.equal(0);
+    });
+    it('clears localstorage data when called twice in parallel', async () => {
+      const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } });
+      await w.loadFile(path.join(fixtures, 'api', 'localstorage.html'));
+
+      expect(await w.webContents.executeJavaScript('localStorage.length')).to.be.greaterThan(0);
+
+      // This first call is not awaited immediately
+      const clearDataPromise = w.webContents.session.clearData();
+      await w.webContents.session.clearData();
+
+      expect(await w.webContents.executeJavaScript('localStorage.length')).to.equal(0);
+
+      // Await the first promise so it doesn't creep into another test
+      await clearDataPromise;
+    });
+  });
 });