From 13a447a89209821b88b13e8fdc97c7ff90e18bff Mon Sep 17 00:00:00 2001 From: Moe Bazzi Date: Tue, 1 Oct 2024 12:31:08 -0400 Subject: [PATCH] Implement browser.storageArea.getKeys() for Web Extension Storage API. https://bugs.webkit.org/show_bug.cgi?id=280275 Reviewed by NOBODY (OOPS!). This patch implements a web extension API to retrieve all keys for a given storage area in the browser.storage API. WECG Proposal: https://github.com/w3c/webextensions/blob/main/proposals/storage-get-keys.md WECG Original Issue: https://github.com/w3c/webextensions/issues/601 * Source/WebKit/UIProcess/Extensions/Cocoa/API/WebExtensionContextAPIStorageCocoa.mm: (WebKit::WebExtensionContext::storageGetKeys): * Source/WebKit/UIProcess/Extensions/Cocoa/_WKWebExtensionStorageSQLiteStore.h: * Source/WebKit/UIProcess/Extensions/Cocoa/_WKWebExtensionStorageSQLiteStore.mm: (-[_WKWebExtensionStorageSQLiteStore getAllKeys:]): (-[_WKWebExtensionStorageSQLiteStore getValuesForKeys:completionHandler:]): * Source/WebKit/UIProcess/Extensions/WebExtensionContext.h: * Source/WebKit/UIProcess/Extensions/WebExtensionContext.messages.in: * Source/WebKit/WebProcess/Extensions/API/Cocoa/WebExtensionAPIStorageAreaCocoa.mm: (WebKit::WebExtensionAPIStorageArea::getKeys): * Source/WebKit/WebProcess/Extensions/API/WebExtensionAPIStorageArea.h: * Source/WebKit/WebProcess/Extensions/Interfaces/WebExtensionAPIStorageArea.idl: * Tools/TestWebKitAPI/Tests/WebKitCocoa/WKWebExtensionAPIStorage.mm: (TestWebKitAPI::TEST(WKWebExtensionAPIStorage, Errors)): (TestWebKitAPI::TEST(WKWebExtensionAPIStorage, GetKeys)): --- .../API/WebExtensionContextAPIStorageCocoa.mm | 13 ++++++++ .../Cocoa/_WKWebExtensionStorageSQLiteStore.h | 1 + .../_WKWebExtensionStorageSQLiteStore.mm | 24 ++++++++++++-- .../Extensions/WebExtensionContext.h | 1 + .../WebExtensionContext.messages.in | 1 + .../Cocoa/WebExtensionAPIStorageAreaCocoa.mm | 12 +++++++ .../API/WebExtensionAPIStorageArea.h | 1 + .../Interfaces/WebExtensionAPIStorageArea.idl | 1 + .../WebKitCocoa/WKWebExtensionAPIStorage.mm | 32 +++++++++++++++++++ 9 files changed, 84 insertions(+), 2 deletions(-) diff --git a/Source/WebKit/UIProcess/Extensions/Cocoa/API/WebExtensionContextAPIStorageCocoa.mm b/Source/WebKit/UIProcess/Extensions/Cocoa/API/WebExtensionContextAPIStorageCocoa.mm index 080963434926e..63ec74d22c350 100644 --- a/Source/WebKit/UIProcess/Extensions/Cocoa/API/WebExtensionContextAPIStorageCocoa.mm +++ b/Source/WebKit/UIProcess/Extensions/Cocoa/API/WebExtensionContextAPIStorageCocoa.mm @@ -64,6 +64,19 @@ }).get()]; } +void WebExtensionContext::storageGetKeys(WebPageProxyIdentifier webPageProxyIdentifier, WebExtensionDataType dataType, CompletionHandler, WebExtensionError>&&)>&& completionHandler) +{ + auto callingAPIName = makeString("browser.storage."_s, toAPIString(dataType), ".getKeys()"_s); + + auto storage = storageForType(dataType); + [storage getAllKeys:makeBlockPtr([callingAPIName, completionHandler = WTFMove(completionHandler)](NSArray *keys, NSString *errorMessage) mutable { + if (errorMessage) + completionHandler(toWebExtensionError(callingAPIName, nil, errorMessage)); + else + completionHandler(makeVector(keys)); + }).get()]; +} + void WebExtensionContext::storageGetBytesInUse(WebPageProxyIdentifier webPageProxyIdentifier, WebExtensionDataType dataType, const Vector& keys, CompletionHandler&&)>&& completionHandler) { auto callingAPIName = makeString("browser.storage."_s, toAPIString(dataType), ".getBytesInUse()"_s); diff --git a/Source/WebKit/UIProcess/Extensions/Cocoa/_WKWebExtensionStorageSQLiteStore.h b/Source/WebKit/UIProcess/Extensions/Cocoa/_WKWebExtensionStorageSQLiteStore.h index 294990c2e3fb1..64fa06f10b108 100644 --- a/Source/WebKit/UIProcess/Extensions/Cocoa/_WKWebExtensionStorageSQLiteStore.h +++ b/Source/WebKit/UIProcess/Extensions/Cocoa/_WKWebExtensionStorageSQLiteStore.h @@ -40,6 +40,7 @@ enum class WebExtensionDataType : uint8_t; - (instancetype)initWithUniqueIdentifier:(NSString *)uniqueIdentifier storageType:(WebKit::WebExtensionDataType)storageType directory:(NSString *)directory usesInMemoryDatabase:(BOOL)useInMemoryDatabase; +- (void)getAllKeys:(void (^)(NSArray *keys, NSString * _Nullable errorMessage))completionHandler; - (void)getValuesForKeys:(NSArray *)keys completionHandler:(void (^)(NSDictionary *results, NSString * _Nullable errorMessage))completionHandler; - (void)getStorageSizeForKeys:(NSArray *)keys completionHandler:(void (^)(size_t storageSize, NSString * _Nullable errorMessage))completionHandler; - (void)getStorageSizeForAllKeysIncludingKeyedData:(NSDictionary *)additionalKeyedData withCompletionHandler:(void (^)(size_t storageSize, NSUInteger numberOfKeysIncludingAdditionalKeyedData, NSDictionary *existingKeysAndValues, NSString * _Nullable errorMessage))completionHandler; diff --git a/Source/WebKit/UIProcess/Extensions/Cocoa/_WKWebExtensionStorageSQLiteStore.mm b/Source/WebKit/UIProcess/Extensions/Cocoa/_WKWebExtensionStorageSQLiteStore.mm index 66a6a597c8bfd..50e267f2682c1 100644 --- a/Source/WebKit/UIProcess/Extensions/Cocoa/_WKWebExtensionStorageSQLiteStore.mm +++ b/Source/WebKit/UIProcess/Extensions/Cocoa/_WKWebExtensionStorageSQLiteStore.mm @@ -71,14 +71,34 @@ - (instancetype)initWithUniqueIdentifier:(NSString *)uniqueIdentifier storageTyp return self; } +- (void)getAllKeys:(void (^)(NSArray *keys, NSString *errorMessage))completionHandler +{ + auto weakSelf = WeakObjCPtr<_WKWebExtensionStorageSQLiteStore> { self }; + dispatch_async(_databaseQueue, ^{ + auto strongSelf = weakSelf.get(); + if (!strongSelf) { + RELEASE_LOG_ERROR(Extensions, "Failed to retrieve all keys for extension %{private}@.", self->_uniqueIdentifier); + completionHandler(nil, @"Failed to retrieve all keys"); + return; + } + + NSString *errorMessage; + auto *keysArray = [self _getAllKeysReturningErrorMessage:&errorMessage].allObjects; + + dispatch_async(dispatch_get_main_queue(), ^{ + completionHandler(keysArray, errorMessage); + }); + }); +} + - (void)getValuesForKeys:(NSArray *)keys completionHandler:(void (^)(NSDictionary *results, NSString *errorMessage))completionHandler { auto weakSelf = WeakObjCPtr<_WKWebExtensionStorageSQLiteStore> { self }; dispatch_async(_databaseQueue, ^{ auto strongSelf = weakSelf.get(); if (!strongSelf) { - RELEASE_LOG_ERROR(Extensions, "Failed to retrieve keys: %{private}@ for extension %{private}@.", keys, self->_uniqueIdentifier); - completionHandler(nil, [NSString stringWithFormat:@"Failed to retrieve keys %@", keys]); + RELEASE_LOG_ERROR(Extensions, "Failed to retrieve values for keys: %{private}@ for extension %{private}@.", keys, self->_uniqueIdentifier); + completionHandler(nil, [NSString stringWithFormat:@"Failed to retrieve values for keys %@", keys]); return; } diff --git a/Source/WebKit/UIProcess/Extensions/WebExtensionContext.h b/Source/WebKit/UIProcess/Extensions/WebExtensionContext.h index 828490c95dfbc..015ebb3d86244 100644 --- a/Source/WebKit/UIProcess/Extensions/WebExtensionContext.h +++ b/Source/WebKit/UIProcess/Extensions/WebExtensionContext.h @@ -857,6 +857,7 @@ class WebExtensionContext : public API::ObjectImpl& keys, CompletionHandler&&)>&&); + void storageGetKeys(WebPageProxyIdentifier, WebExtensionDataType, CompletionHandler, WebExtensionError>&&)>&&); void storageGetBytesInUse(WebPageProxyIdentifier, WebExtensionDataType, const Vector& keys, CompletionHandler&&)>&&); void storageSet(WebPageProxyIdentifier, WebExtensionDataType, const String& dataJSON, CompletionHandler&&)>&&); void storageRemove(WebPageProxyIdentifier, WebExtensionDataType, const Vector& keys, CompletionHandler&&)>&&); diff --git a/Source/WebKit/UIProcess/Extensions/WebExtensionContext.messages.in b/Source/WebKit/UIProcess/Extensions/WebExtensionContext.messages.in index ee1081cfac3d0..a95444bda6054 100644 --- a/Source/WebKit/UIProcess/Extensions/WebExtensionContext.messages.in +++ b/Source/WebKit/UIProcess/Extensions/WebExtensionContext.messages.in @@ -133,6 +133,7 @@ messages -> WebExtensionContext { // Storage APIs [EnabledIf='isStorageMessageAllowed()'] StorageGet(WebKit::WebPageProxyIdentifier webPageProxyIdentifier, WebKit::WebExtensionDataType dataType, Vector keys) -> (Expected result) + [EnabledIf='isStorageMessageAllowed()'] StorageGetKeys(WebKit::WebPageProxyIdentifier webPageProxyIdentifier, WebKit::WebExtensionDataType dataType) -> (Expected, WebKit::WebExtensionError> result) [EnabledIf='isStorageMessageAllowed()'] StorageGetBytesInUse(WebKit::WebPageProxyIdentifier webPageProxyIdentifier, WebKit::WebExtensionDataType dataType, Vector keys) -> (Expected result) [EnabledIf='isStorageMessageAllowed()'] StorageSet(WebKit::WebPageProxyIdentifier webPageProxyIdentifier, WebKit::WebExtensionDataType dataType, String dataJSON) -> (Expected result) [EnabledIf='isStorageMessageAllowed()'] StorageRemove(WebKit::WebPageProxyIdentifier webPageProxyIdentifier, WebKit::WebExtensionDataType dataType, Vector keys) -> (Expected result) diff --git a/Source/WebKit/WebProcess/Extensions/API/Cocoa/WebExtensionAPIStorageAreaCocoa.mm b/Source/WebKit/WebProcess/Extensions/API/Cocoa/WebExtensionAPIStorageAreaCocoa.mm index a0a40370e8302..2b890ea510730 100644 --- a/Source/WebKit/WebProcess/Extensions/API/Cocoa/WebExtensionAPIStorageAreaCocoa.mm +++ b/Source/WebKit/WebProcess/Extensions/API/Cocoa/WebExtensionAPIStorageAreaCocoa.mm @@ -116,6 +116,18 @@ }, extensionContext().identifier()); } +void WebExtensionAPIStorageArea::getKeys(WebPage& page, Ref&& callback, NSString **outExceptionString) +{ + WebProcess::singleton().sendWithAsyncReply(Messages::WebExtensionContext::StorageGetKeys(page.webPageProxyIdentifier(), m_type), [protectedThis = Ref { *this }, callback = WTFMove(callback)](Expected, WebExtensionError>&& result) { + if (!result) { + callback->reportError(result.error()); + return; + } + + callback->call(createNSArray(result.value()).get()); + }, extensionContext().identifier()); +} + void WebExtensionAPIStorageArea::getBytesInUse(WebPage& page, id keys, Ref&& callback, NSString **outExceptionString) { // Documentation: https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/storage/StorageArea/getBytesInUse diff --git a/Source/WebKit/WebProcess/Extensions/API/WebExtensionAPIStorageArea.h b/Source/WebKit/WebProcess/Extensions/API/WebExtensionAPIStorageArea.h index 6c1777160884a..967bbe7ae7316 100644 --- a/Source/WebKit/WebProcess/Extensions/API/WebExtensionAPIStorageArea.h +++ b/Source/WebKit/WebProcess/Extensions/API/WebExtensionAPIStorageArea.h @@ -43,6 +43,7 @@ class WebExtensionAPIStorageArea : public WebExtensionAPIObject, public JSWebExt bool isPropertyAllowed(const ASCIILiteral& propertyName, WebPage*); void get(WebPage&, id items, Ref&&, NSString **outExceptionString); + void getKeys(WebPage&, Ref&&, NSString **outExceptionString); void getBytesInUse(WebPage&, id keys, Ref&&, NSString **outExceptionString); void set(WebPage&, NSDictionary *items, Ref&&, NSString **outExceptionString); void remove(WebPage&, id keys, Ref&&, NSString **outExceptionString); diff --git a/Source/WebKit/WebProcess/Extensions/Interfaces/WebExtensionAPIStorageArea.idl b/Source/WebKit/WebProcess/Extensions/Interfaces/WebExtensionAPIStorageArea.idl index 82a3caba87f7a..f256d5d2283dd 100644 --- a/Source/WebKit/WebProcess/Extensions/Interfaces/WebExtensionAPIStorageArea.idl +++ b/Source/WebKit/WebProcess/Extensions/Interfaces/WebExtensionAPIStorageArea.idl @@ -30,6 +30,7 @@ ] interface WebExtensionAPIStorageArea { [RaisesException] void get([Optional, NSObject, DOMString] any items, [Optional, CallbackHandler] function callback); + [RaisesException] void getKeys([Optional, CallbackHandler] function callback); [RaisesException] void getBytesInUse([Optional, NSObject, DOMString] any keys, [Optional, CallbackHandler] function callback); [RaisesException] void set([NSDictionary=StopAtTopLevel] any items, [Optional, CallbackHandler] function callback); [RaisesException] void remove([NSObject] any keys, [Optional, CallbackHandler] function callback); diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/WKWebExtensionAPIStorage.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/WKWebExtensionAPIStorage.mm index e5eb5b76a0f57..b48d34e5f7e35 100644 --- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/WKWebExtensionAPIStorage.mm +++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/WKWebExtensionAPIStorage.mm @@ -59,6 +59,8 @@ auto *backgroundScript = Util::constructScript(@[ @"browser.test.assertThrows(() => browser?.storage?.local?.get(Date.now()), /'items' value is invalid, because an object or a string or an array of strings or null is expected, but a number was provided/i)", + @"browser.test.assertThrows(() => browser?.storage?.local?.getKeys('invalid'), /'callback' value is invalid, because a function is expected/i)", + @"browser.test.assertThrows(() => browser?.storage?.local?.getBytesInUse({}), /'keys' value is invalid, because a string or an array of strings or null is expected, but an object was provided/i)", @"browser.test.assertThrows(() => browser?.storage?.local?.getBytesInUse([1]), /'keys' value is invalid, because a string or an array of strings or null is expected, but an array of other values was provided/i)", @@ -292,6 +294,36 @@ Util::loadAndRunExtension(storageManifest, @{ @"background.js": backgroundScript }); } +TEST(WKWebExtensionAPIStorage, GetKeys) +{ + auto *backgroundScript = Util::constructScript(@[ + @"const data = { 'string': 'string', 'number': 1, 'boolean': true, 'dictionary': {'key': 'value'}, 'array': [1, true, 'string'], 'null': null }", + @"await browser?.storage?.local?.set(data)", + + @"var keys = await browser?.storage?.local?.getKeys()", + @"browser.test.assertEq(keys.length, 6, 'Should have 6 keys')", + @"browser.test.assertTrue(keys.includes('string'), 'Should include string key')", + @"browser.test.assertTrue(keys.includes('number'), 'Should include number key')", + @"browser.test.assertTrue(keys.includes('boolean'), 'Should include boolean key')", + @"browser.test.assertTrue(keys.includes('dictionary'), 'Should include dictionary key')", + @"browser.test.assertTrue(keys.includes('array'), 'Should include array key')", + @"browser.test.assertTrue(keys.includes('null'), 'Should include null key')", + + @"await browser?.storage?.local?.remove('number')", + @"keys = await browser?.storage?.local?.getKeys()", + @"browser.test.assertEq(keys.length, 5, 'Should have 5 keys after removal')", + @"browser.test.assertFalse(keys.includes('number'), 'Should not include removed number key')", + + @"await browser?.storage?.local?.clear()", + @"keys = await browser?.storage?.local?.getKeys()", + @"browser.test.assertEq(keys.length, 0, 'Should have no keys after clear')", + + @"browser.test.notifyPass()", + ]); + + Util::loadAndRunExtension(storageManifest, @{ @"background.js": backgroundScript }); +} + TEST(WKWebExtensionAPIStorage, GetBytesInUse) { auto *backgroundScript = Util::constructScript(@[