0

[Extensions] Allow extensions to update script worlds to default world

Extensions can inject in multiple user script worlds, which are
specified by different user script world (string) IDs. If an ID isn't
specified, the extension injects in the default user script world ID.
However, this creates an issue with trying to update a registered
script to inject in the default world after initially registered with
a non-default world:
* Specifying any non-null value indicates a non-default world
* Specifying a null value will be dropped by the extension bindings

To enable this, allow extensions to use the empty string ('') as an
indicator that a script should use the default world. The API layer
converts the empty string to nullopt for use elsewhere in the
extensions system.

Bug: 331680187
Change-Id: I4b0ef064884dc5fac814c0680134c278d0b28fcc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6036255
Reviewed-by: Kelvin Jiang <kelvinjiang@chromium.org>
Commit-Queue: Devlin Cronin <rdevlin.cronin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1387164}
This commit is contained in:
Devlin Cronin
2024-11-23 00:20:43 +00:00
committed by Chromium LUCI CQ
parent 867fcb1baa
commit bc782f1740
5 changed files with 76 additions and 12 deletions
chrome/test/data/extensions/api_test/user_scripts
configure_world
get_and_remove_worlds
update
extensions/browser

@ -62,9 +62,6 @@ async function cleanUpState() {
chrome.test.runTests([ chrome.test.runTests([
async function UserScriptWorld_worldIdValidation() { async function UserScriptWorld_worldIdValidation() {
await chrome.test.assertPromiseRejects(
chrome.userScripts.configureWorld({csp: '', worldId: ''}),
'Error: If specified, `worldId` must be non-empty.');
await chrome.test.assertPromiseRejects( await chrome.test.assertPromiseRejects(
chrome.userScripts.configureWorld({csp: '', worldId: '_foobar'}), chrome.userScripts.configureWorld({csp: '', worldId: '_foobar'}),
`Error: World IDs beginning with '_' are reserved.`); `Error: World IDs beginning with '_' are reserved.`);

@ -51,6 +51,27 @@ chrome.test.runTests([
chrome.test.succeed(); chrome.test.succeed();
}, },
async function emptyWorldIdMapsToDefaultWorld() {
let defaultWorldWithEmptyIdConfig = { ...configForDefaultWorld };
defaultWorldWithEmptyIdConfig.worldId = '';
// Assign a config for a world with ''. This will map to the default world
// (where `worldId` is omitted).
await chrome.userScripts.configureWorld(defaultWorldWithEmptyIdConfig);
let worlds = await chrome.userScripts.getWorldConfigurations();
chrome.test.assertEq([configForDefaultWorld], worlds);
// Remove the default world configuration by removing the config for the
// empty-string world.
await chrome.userScripts.resetWorldConfiguration('');
// There should no longer be any registered configurations.
worlds = await chrome.userScripts.getWorldConfigurations();
chrome.test.assertEq([], worlds);
chrome.test.succeed();
},
async function retrieveAndRemoveAdditionalWorldConfig() { async function retrieveAndRemoveAdditionalWorldConfig() {
// Add a non-default world config. // Add a non-default world config.
await chrome.userScripts.configureWorld(configForOtherWorld); await chrome.userScripts.configureWorld(configForOtherWorld);
@ -96,10 +117,6 @@ chrome.test.runTests([
}, },
async function callingResetWithInvalidIdsFails() { async function callingResetWithInvalidIdsFails() {
await chrome.test.assertPromiseRejects(
chrome.userScripts.resetWorldConfiguration(''),
'Error: If specified, `worldId` must be non-empty.');
await chrome.test.assertPromiseRejects( await chrome.test.assertPromiseRejects(
chrome.userScripts.resetWorldConfiguration('_foo'), chrome.userScripts.resetWorldConfiguration('_foo'),
`Error: World IDs beginning with '_' are reserved.`); `Error: World IDs beginning with '_' are reserved.`);

@ -197,6 +197,50 @@ chrome.test.runTests([
chrome.test.succeed(); chrome.test.succeed();
}, },
// Tests updating a user script world ID from a non-default world back to
// the default world.
async function updateUserScriptToDefaultWorldId() {
await chrome.userScripts.unregister();
// Register a user script with a given world ID.
const scriptToRegister = [
{
id: 'us1',
matches: ['*://*/*'],
js: [{file: 'user_script.js'}],
worldId: 'some world',
},
];
await chrome.userScripts.register(scriptToRegister);
// Update it to the default world by specifying `worldId: ''`.
const scriptUpdate = [
{
id: 'us1',
matches: ['*://*/*'],
js: [{file: 'user_script.js'}],
worldId: '',
}
];
await chrome.userScripts.update(scriptUpdate);
// The updated script should now use the default world ID, which is
// represented by not having a specified world ID.
const expectedScripts = [{
id: 'us1',
matches: ['*://*/*'],
js: [{file: 'user_script.js'}],
runAt: 'document_idle',
allFrames: false,
world: 'USER_SCRIPT',
}];
const registeredScripts = await chrome.userScripts.getScripts();
chrome.test.assertEq(expectedScripts, registeredScripts);
chrome.test.succeed();
},
// Tests that calling userScripts.update with a specific ID updates such // Tests that calling userScripts.update with a specific ID updates such
// script and does not inject them into a (former) matching frame. // script and does not inject them into a (former) matching frame.
async function scriptUpdated() { async function scriptUpdated() {

@ -41,9 +41,9 @@ constexpr char kInvalidSourceError[] =
constexpr char kMatchesMissingError[] = constexpr char kMatchesMissingError[] =
"User script with ID '*' must specify 'matches'."; "User script with ID '*' must specify 'matches'.";
// Returns true if the given `world_id` is valid from the API perspective. // Sanitizes the given `world_id`, updating it if necessary.
// If invalid, populates `error_out`. // Returns true on success; on failure, returns false and populates `error_out`.
bool IsValidWorldId(const std::optional<std::string>& world_id, bool IsValidWorldId(std::optional<std::string>& world_id,
std::string* error_out) { std::string* error_out) {
if (!world_id) { if (!world_id) {
// Omitting world ID is valid. // Omitting world ID is valid.
@ -51,8 +51,11 @@ bool IsValidWorldId(const std::optional<std::string>& world_id,
} }
if (world_id->empty()) { if (world_id->empty()) {
*error_out = "If specified, `worldId` must be non-empty."; // Specifying an empty-string world ID is valid, and will use the default
return false; // user script world. This is represented by nullopt elsewhere, so we update
// the world ID value.
world_id = std::nullopt;
return true;
} }
if (world_id->at(0) == '_') { if (world_id->at(0) == '_') {

@ -106,6 +106,7 @@ void UserScriptWorldConfigurationManager::SetUserScriptWorldInfo(
const std::optional<std::string>& world_id, const std::optional<std::string>& world_id,
std::optional<std::string> csp, std::optional<std::string> csp,
bool enable_messaging) { bool enable_messaging) {
CHECK(!world_id || !world_id->empty());
// Persist world configuratation in ExtensionPrefs. // Persist world configuratation in ExtensionPrefs.
ExtensionPrefs::ScopedDictionaryUpdate update( ExtensionPrefs::ScopedDictionaryUpdate update(
extension_prefs_, extension.id(), kUserScriptsWorldsConfiguration.name); extension_prefs_, extension.id(), kUserScriptsWorldsConfiguration.name);
@ -130,6 +131,7 @@ void UserScriptWorldConfigurationManager::SetUserScriptWorldInfo(
void UserScriptWorldConfigurationManager::ClearUserScriptWorldInfo( void UserScriptWorldConfigurationManager::ClearUserScriptWorldInfo(
const Extension& extension, const Extension& extension,
const std::optional<std::string>& world_id) { const std::optional<std::string>& world_id) {
CHECK(!world_id || !world_id->empty());
ExtensionPrefs::ScopedDictionaryUpdate update( ExtensionPrefs::ScopedDictionaryUpdate update(
extension_prefs_, extension.id(), kUserScriptsWorldsConfiguration.name); extension_prefs_, extension.id(), kUserScriptsWorldsConfiguration.name);
std::unique_ptr<prefs::DictionaryValueUpdate> update_dict = update.Get(); std::unique_ptr<prefs::DictionaryValueUpdate> update_dict = update.Get();
@ -151,6 +153,7 @@ mojom::UserScriptWorldInfoPtr
UserScriptWorldConfigurationManager::GetUserScriptWorldInfo( UserScriptWorldConfigurationManager::GetUserScriptWorldInfo(
const ExtensionId& extension_id, const ExtensionId& extension_id,
const std::optional<std::string>& world_id) { const std::optional<std::string>& world_id) {
CHECK(!world_id || !world_id->empty());
const base::Value::Dict* worlds_configuration = const base::Value::Dict* worlds_configuration =
extension_prefs_->ReadPrefAsDictionary(extension_id, extension_prefs_->ReadPrefAsDictionary(extension_id,
kUserScriptsWorldsConfiguration); kUserScriptsWorldsConfiguration);