mirror of
https://github.com/electron/electron.git
synced 2026-10-28 11:44:46 +08:00
feat: support chrome.storage.sync in extensions (#54088)
* feat: support chrome.storage.sync in extensions Register a ValueStoreCache for the SYNC settings namespace from ElectronExtensionsAPIClient::AddAdditionalValueStoreCaches. Until now Electron never overrode that hook, so StorageFrontend only knew about LOCAL (and the in-memory SESSION area) and every chrome.storage.sync call failed with '"sync" is not available in this instance of Chrome'. ElectronSyncValueStoreCache mirrors Chromium's LocalValueStoreCache but uses settings_namespace::SYNC, which gives it its own LevelDB directory under the partition path (kSyncExtensionSettingsDirectoryName), so sync and local never alias. It enforces the chrome.storage.sync quotas from extensions/common/api/storage.json (QUOTA_BYTES 102400, QUOTA_BYTES_PER_ITEM 8192, MAX_ITEMS 512); the per-minute and per-hour write limits are already applied to kSync by storage_api.cc. There is no sync service in Electron, so the area behaves like Chrome with sync disabled: a second local store with no cross-device sync. chrome.storage.managed stays unsupported because there is no policy source to back it. This answers the question raised on #53995 about supporting chrome.storage instead of dropping sync from the test fixture. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AFucMLGyVVqSUAKXrqbaUT * fix: include value_store_factory.h in ElectronExtensionsAPIClient The linux-x64 and linux-x64-ubsan builds failed compiling electron_extensions_api_client.cc: ../../base/memory/scoped_refptr.h:398:6: error: member access into incomplete type 'value_store::ValueStoreFactory' ../../base/memory/scoped_refptr.h:405:6: error: member access into incomplete type 'value_store::ValueStoreFactory' note: in instantiation of ... requested here electron_extensions_api_client.cc:97:39: new ElectronSyncValueStoreCache(factory); AddAdditionalValueStoreCaches copies the scoped_refptr<value_store::ValueStoreFactory> into the new ElectronSyncValueStoreCache, which instantiates AddRef/Release on the pointee. extensions_api_client.h and electron_sync_value_store_cache.h only forward-declare ValueStoreFactory, so the translation unit needs the full definition. Include components/value_store/value_store_factory.h the same way chrome_extensions_api_client.cc does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AFucMLGyVVqSUAKXrqbaUT * test: use vitest matchers in chrome.storage.sync spec main dropped chai in favour of vitest's own matchers and now lints against chai-style chains, so convert the chrome.storage.sync spec's assertions to the equivalent vitest matchers. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AFucMLGyVVqSUAKXrqbaUT --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
0e77ca92cd
commit
2fb6c26db6
9 files changed
+290
-1
No files matched your search
@@ -144,8 +144,12 @@ See [official documentation](https://developer.chrome.com/docs/extensions/refere
|
||||
The following methods of `chrome.storage` are supported:
|
||||
|
||||
- `chrome.storage.local`
|
||||
- `chrome.storage.sync` (partial support)
|
||||
- stored on disk next to `chrome.storage.local` in a separate storage area
|
||||
and subject to Chrome's `sync` quotas; data is **not** synced across
|
||||
devices, matching Chrome with sync disabled.
|
||||
|
||||
`chrome.storage.sync` and `chrome.storage.managed` are **not** supported.
|
||||
`chrome.storage.managed` is **not** supported.
|
||||
|
||||
See [official documentation](https://developer.chrome.com/docs/extensions/reference/storage) for more information.
|
||||
|
||||
|
||||
@@ -862,6 +862,8 @@ filenames = {
|
||||
"shell/browser/extensions/api/resources_private/resources_private_api.h",
|
||||
"shell/browser/extensions/api/runtime/electron_runtime_api_delegate.cc",
|
||||
"shell/browser/extensions/api/runtime/electron_runtime_api_delegate.h",
|
||||
"shell/browser/extensions/api/storage/electron_sync_value_store_cache.cc",
|
||||
"shell/browser/extensions/api/storage/electron_sync_value_store_cache.h",
|
||||
"shell/browser/extensions/api/streams_private/streams_private_api.cc",
|
||||
"shell/browser/extensions/api/streams_private/streams_private_api.h",
|
||||
"shell/browser/extensions/api/tabs/tabs_api.cc",
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2026 Anthropic, PBC.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "shell/browser/extensions/api/storage/electron_sync_value_store_cache.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "components/value_store/value_store.h"
|
||||
#include "components/value_store/value_store_factory.h"
|
||||
#include "content/public/browser/browser_thread.h"
|
||||
#include "extensions/browser/api/storage/backend_task_runner.h"
|
||||
#include "extensions/browser/api/storage/settings_namespace.h"
|
||||
#include "extensions/browser/api/storage/value_store_util.h"
|
||||
#include "extensions/common/api/storage.h"
|
||||
#include "extensions/common/extension.h"
|
||||
|
||||
namespace extensions {
|
||||
|
||||
namespace {
|
||||
|
||||
// Returns the quota limits for sync storage, taken from the schema in
|
||||
// extensions/common/api/storage.json.
|
||||
SettingsStorageQuotaEnforcer::Limits GetSyncQuotaLimits() {
|
||||
return {static_cast<size_t>(api::storage::sync::QUOTA_BYTES),
|
||||
static_cast<size_t>(api::storage::sync::QUOTA_BYTES_PER_ITEM),
|
||||
static_cast<size_t>(api::storage::sync::MAX_ITEMS)};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ElectronSyncValueStoreCache::ElectronSyncValueStoreCache(
|
||||
scoped_refptr<value_store::ValueStoreFactory> factory)
|
||||
: storage_factory_(std::move(factory)), quota_(GetSyncQuotaLimits()) {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
}
|
||||
|
||||
ElectronSyncValueStoreCache::~ElectronSyncValueStoreCache() {
|
||||
DCHECK(IsOnBackendSequence());
|
||||
}
|
||||
|
||||
void ElectronSyncValueStoreCache::RunWithValueStoreForExtension(
|
||||
StorageCallback callback,
|
||||
scoped_refptr<const Extension> extension) {
|
||||
DCHECK(IsOnBackendSequence());
|
||||
// Unlike LOCAL, the unlimitedStorage permission does not lift the sync
|
||||
// quotas, so the enforcer is always handed out directly.
|
||||
std::move(callback).Run(GetStorage(extension.get()));
|
||||
}
|
||||
|
||||
void ElectronSyncValueStoreCache::DeleteStorageSoon(
|
||||
const ExtensionId& extension_id) {
|
||||
DCHECK(IsOnBackendSequence());
|
||||
storage_map_.erase(extension_id);
|
||||
|
||||
value_store_util::DeleteValueStore(settings_namespace::SYNC,
|
||||
value_store_util::ModelType::APP,
|
||||
extension_id, storage_factory_);
|
||||
|
||||
value_store_util::DeleteValueStore(settings_namespace::SYNC,
|
||||
value_store_util::ModelType::EXTENSION,
|
||||
extension_id, storage_factory_);
|
||||
}
|
||||
|
||||
value_store::ValueStore* ElectronSyncValueStoreCache::GetStorage(
|
||||
const Extension* extension) {
|
||||
auto iter = storage_map_.find(extension->id());
|
||||
if (iter != storage_map_.end())
|
||||
return iter->second.get();
|
||||
|
||||
value_store_util::ModelType model_type =
|
||||
extension->is_app() ? value_store_util::ModelType::APP
|
||||
: value_store_util::ModelType::EXTENSION;
|
||||
std::unique_ptr<value_store::ValueStore> store =
|
||||
value_store_util::CreateSettingsStore(settings_namespace::SYNC,
|
||||
model_type, extension->id(),
|
||||
storage_factory_);
|
||||
auto storage =
|
||||
std::make_unique<SettingsStorageQuotaEnforcer>(quota_, std::move(store));
|
||||
DCHECK(storage);
|
||||
|
||||
value_store::ValueStore* storage_ptr = storage.get();
|
||||
storage_map_[extension->id()] = std::move(storage);
|
||||
return storage_ptr;
|
||||
}
|
||||
|
||||
} // namespace extensions
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) 2026 Anthropic, PBC.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef ELECTRON_SHELL_BROWSER_EXTENSIONS_API_STORAGE_ELECTRON_SYNC_VALUE_STORE_CACHE_H_
|
||||
#define ELECTRON_SHELL_BROWSER_EXTENSIONS_API_STORAGE_ELECTRON_SYNC_VALUE_STORE_CACHE_H_
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
|
||||
#include "base/memory/scoped_refptr.h"
|
||||
#include "extensions/browser/api/storage/settings_storage_quota_enforcer.h"
|
||||
#include "extensions/browser/api/storage/value_store_cache.h"
|
||||
#include "extensions/common/extension_id.h"
|
||||
|
||||
namespace value_store {
|
||||
class ValueStore;
|
||||
class ValueStoreFactory;
|
||||
} // namespace value_store
|
||||
|
||||
namespace extensions {
|
||||
|
||||
// ValueStoreCache for the SYNC namespace. Electron has no sync backend, so
|
||||
// this behaves like Chrome with sync disabled: a second on-disk storage area,
|
||||
// separate from LOCAL, that enforces the chrome.storage.sync quotas but never
|
||||
// leaves the device.
|
||||
class ElectronSyncValueStoreCache : public ValueStoreCache {
|
||||
public:
|
||||
explicit ElectronSyncValueStoreCache(
|
||||
scoped_refptr<value_store::ValueStoreFactory> factory);
|
||||
|
||||
ElectronSyncValueStoreCache(const ElectronSyncValueStoreCache&) = delete;
|
||||
ElectronSyncValueStoreCache& operator=(const ElectronSyncValueStoreCache&) =
|
||||
delete;
|
||||
|
||||
~ElectronSyncValueStoreCache() override;
|
||||
|
||||
// ValueStoreCache implementation:
|
||||
void RunWithValueStoreForExtension(
|
||||
StorageCallback callback,
|
||||
scoped_refptr<const Extension> extension) override;
|
||||
void DeleteStorageSoon(const ExtensionId& extension_id) override;
|
||||
|
||||
private:
|
||||
using StorageMap =
|
||||
std::map<ExtensionId, std::unique_ptr<value_store::ValueStore>>;
|
||||
|
||||
value_store::ValueStore* GetStorage(const Extension* extension);
|
||||
|
||||
// The factory to use for creating new ValueStores.
|
||||
const scoped_refptr<value_store::ValueStoreFactory> storage_factory_;
|
||||
|
||||
// Quota limits (see SettingsStorageQuotaEnforcer).
|
||||
const SettingsStorageQuotaEnforcer::Limits quota_;
|
||||
|
||||
// The collection of ValueStores for sync storage.
|
||||
StorageMap storage_map_;
|
||||
};
|
||||
|
||||
} // namespace extensions
|
||||
|
||||
#endif // ELECTRON_SHELL_BROWSER_EXTENSIONS_API_STORAGE_ELECTRON_SYNC_VALUE_STORE_CACHE_H_
|
||||
@@ -4,14 +4,18 @@
|
||||
|
||||
#include "shell/browser/extensions/electron_extensions_api_client.h"
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "components/value_store/value_store_factory.h"
|
||||
#include "extensions/browser/api/storage/settings_namespace.h"
|
||||
#include "extensions/browser/guest_view/extensions_guest_view_manager_delegate.h"
|
||||
#include "extensions/browser/guest_view/mime_handler_view/mime_handler_view_guest_delegate.h"
|
||||
#include "printing/buildflags/buildflags.h"
|
||||
#include "shell/browser/api/electron_api_web_contents.h"
|
||||
#include "shell/browser/extensions/api/management/electron_management_api_delegate.h"
|
||||
#include "shell/browser/extensions/api/storage/electron_sync_value_store_cache.h"
|
||||
#include "shell/browser/extensions/electron_extension_web_contents_observer.h"
|
||||
#include "shell/browser/extensions/electron_messaging_delegate.h"
|
||||
#include "shell/common/gin_helper/handle.h"
|
||||
@@ -80,6 +84,20 @@ class ElectronMimeHandlerViewGuestDelegate
|
||||
ElectronExtensionsAPIClient::ElectronExtensionsAPIClient() = default;
|
||||
ElectronExtensionsAPIClient::~ElectronExtensionsAPIClient() = default;
|
||||
|
||||
void ElectronExtensionsAPIClient::AddAdditionalValueStoreCaches(
|
||||
content::BrowserContext* context,
|
||||
const scoped_refptr<value_store::ValueStoreFactory>& factory,
|
||||
SettingsChangedCallback observer,
|
||||
std::map<settings_namespace::Namespace,
|
||||
raw_ptr<ValueStoreCache, CtnExperimental>>* caches) {
|
||||
// Add support for chrome.storage.sync. There is no sync service in
|
||||
// Electron, so the cache is backed by a second local store, the same as
|
||||
// Chrome when sync is disabled. chrome.storage.managed stays unsupported
|
||||
// because there is no policy source to read from.
|
||||
(*caches)[settings_namespace::SYNC] =
|
||||
new ElectronSyncValueStoreCache(factory);
|
||||
}
|
||||
|
||||
MessagingDelegate* ElectronExtensionsAPIClient::GetMessagingDelegate() {
|
||||
if (!messaging_delegate_)
|
||||
messaging_delegate_ = std::make_unique<ElectronMessagingDelegate>();
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#ifndef ELECTRON_SHELL_BROWSER_EXTENSIONS_ELECTRON_EXTENSIONS_API_CLIENT_H_
|
||||
#define ELECTRON_SHELL_BROWSER_EXTENSIONS_ELECTRON_EXTENSIONS_API_CLIENT_H_
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
|
||||
#include "extensions/browser/api/extensions_api_client.h"
|
||||
@@ -19,6 +20,12 @@ class ElectronExtensionsAPIClient : public ExtensionsAPIClient {
|
||||
~ElectronExtensionsAPIClient() override;
|
||||
|
||||
// ExtensionsAPIClient
|
||||
void AddAdditionalValueStoreCaches(
|
||||
content::BrowserContext* context,
|
||||
const scoped_refptr<value_store::ValueStoreFactory>& factory,
|
||||
SettingsChangedCallback observer,
|
||||
std::map<settings_namespace::Namespace,
|
||||
raw_ptr<ValueStoreCache, CtnExperimental>>* caches) override;
|
||||
MessagingDelegate* GetMessagingDelegate() override;
|
||||
void AttachWebContentsHelpers(
|
||||
content::WebContents* web_contents) const override;
|
||||
|
||||
@@ -453,6 +453,42 @@ describe('chrome extensions', () => {
|
||||
w.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
it('supports chrome.storage.sync as a storage area separate from local', async () => {
|
||||
const customSession = session.fromPartition(`persist:${randomUUID()}`);
|
||||
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-storage-sync'));
|
||||
const w = new BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: { session: customSession, nodeIntegration: true, contextIsolation: false }
|
||||
});
|
||||
try {
|
||||
const p = once(ipcMain, 'storage-sync-result');
|
||||
await w.loadURL(url);
|
||||
const [, json] = await p;
|
||||
const result = JSON.parse(json);
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.change).toEqual({
|
||||
key: { newValue: 'sync-value' },
|
||||
other: { newValue: 'sync-other' }
|
||||
});
|
||||
expect(result.sync).toEqual({ key: 'sync-value', other: 'sync-other' });
|
||||
expect(result.local).toEqual({ key: 'local-value' });
|
||||
expect(result.syncAfterRemove).toEqual({ key: 'sync-value' });
|
||||
expect(result.bytesInUse).toBeTypeOf('number');
|
||||
expect(result.bytesInUse).toBeGreaterThan(0);
|
||||
expect(result.perItemError).toBeTypeOf('string');
|
||||
expect(result.perItemError).toContain('kQuotaBytesPerItem quota exceeded');
|
||||
expect(result.syncAfterClear).toEqual({});
|
||||
expect(result.localAfterSyncClear).toEqual({ key: 'local-value' });
|
||||
expect(result.constants).toEqual({
|
||||
QUOTA_BYTES: 102400,
|
||||
QUOTA_BYTES_PER_ITEM: 8192,
|
||||
MAX_ITEMS: 512
|
||||
});
|
||||
} finally {
|
||||
w.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('chrome.webRequest', () => {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/* global chrome */
|
||||
const call = (area, method, ...args) =>
|
||||
new Promise((resolve, reject) => {
|
||||
area[method](...args, (result) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(new Error(chrome.runtime.lastError.message));
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const firstSyncChange = new Promise((resolve) => {
|
||||
chrome.storage.onChanged.addListener((changes, areaName) => {
|
||||
if (areaName === 'sync') resolve(changes);
|
||||
});
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const { sync, local } = chrome.storage;
|
||||
const result = {};
|
||||
try {
|
||||
await call(sync, 'set', { key: 'sync-value', other: 'sync-other' });
|
||||
result.change = await firstSyncChange;
|
||||
await call(local, 'set', { key: 'local-value' });
|
||||
|
||||
result.sync = await call(sync, 'get', null);
|
||||
result.local = await call(local, 'get', null);
|
||||
|
||||
await call(sync, 'remove', 'other');
|
||||
result.syncAfterRemove = await call(sync, 'get', null);
|
||||
|
||||
result.bytesInUse = await call(sync, 'getBytesInUse', null);
|
||||
|
||||
try {
|
||||
await call(sync, 'set', { big: 'x'.repeat(sync.QUOTA_BYTES_PER_ITEM) });
|
||||
result.perItemError = null;
|
||||
} catch (error) {
|
||||
result.perItemError = error.message;
|
||||
}
|
||||
|
||||
await call(sync, 'clear');
|
||||
result.syncAfterClear = await call(sync, 'get', null);
|
||||
result.localAfterSyncClear = await call(local, 'get', null);
|
||||
|
||||
result.constants = {
|
||||
QUOTA_BYTES: sync.QUOTA_BYTES,
|
||||
QUOTA_BYTES_PER_ITEM: sync.QUOTA_BYTES_PER_ITEM,
|
||||
MAX_ITEMS: sync.MAX_ITEMS
|
||||
};
|
||||
} catch (error) {
|
||||
result.error = error.message;
|
||||
}
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.textContent = `require('electron').ipcRenderer.send('storage-sync-result', ${JSON.stringify(JSON.stringify(result))})`;
|
||||
document.documentElement.appendChild(script);
|
||||
})();
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "chrome-storage-sync",
|
||||
"version": "1.0",
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["main.js"],
|
||||
"run_at": "document_start"
|
||||
}
|
||||
],
|
||||
"permissions": [
|
||||
"storage"
|
||||
],
|
||||
"manifest_version": 2
|
||||
}
|
||||
Reference in new issue
Block a user