[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:

committed by
Chromium LUCI CQ

parent
867fcb1baa
commit
bc782f1740
chrome/test/data/extensions/api_test/user_scripts
extensions/browser
@ -62,9 +62,6 @@ async function cleanUpState() {
|
||||
|
||||
chrome.test.runTests([
|
||||
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(
|
||||
chrome.userScripts.configureWorld({csp: '', worldId: '_foobar'}),
|
||||
`Error: World IDs beginning with '_' are reserved.`);
|
||||
|
@ -51,6 +51,27 @@ chrome.test.runTests([
|
||||
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() {
|
||||
// Add a non-default world config.
|
||||
await chrome.userScripts.configureWorld(configForOtherWorld);
|
||||
@ -96,10 +117,6 @@ chrome.test.runTests([
|
||||
},
|
||||
|
||||
async function callingResetWithInvalidIdsFails() {
|
||||
await chrome.test.assertPromiseRejects(
|
||||
chrome.userScripts.resetWorldConfiguration(''),
|
||||
'Error: If specified, `worldId` must be non-empty.');
|
||||
|
||||
await chrome.test.assertPromiseRejects(
|
||||
chrome.userScripts.resetWorldConfiguration('_foo'),
|
||||
`Error: World IDs beginning with '_' are reserved.`);
|
||||
|
@ -197,6 +197,50 @@ chrome.test.runTests([
|
||||
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
|
||||
// script and does not inject them into a (former) matching frame.
|
||||
async function scriptUpdated() {
|
||||
|
@ -41,9 +41,9 @@ constexpr char kInvalidSourceError[] =
|
||||
constexpr char kMatchesMissingError[] =
|
||||
"User script with ID '*' must specify 'matches'.";
|
||||
|
||||
// Returns true if the given `world_id` is valid from the API perspective.
|
||||
// If invalid, populates `error_out`.
|
||||
bool IsValidWorldId(const std::optional<std::string>& world_id,
|
||||
// Sanitizes the given `world_id`, updating it if necessary.
|
||||
// Returns true on success; on failure, returns false and populates `error_out`.
|
||||
bool IsValidWorldId(std::optional<std::string>& world_id,
|
||||
std::string* error_out) {
|
||||
if (!world_id) {
|
||||
// Omitting world ID is valid.
|
||||
@ -51,8 +51,11 @@ bool IsValidWorldId(const std::optional<std::string>& world_id,
|
||||
}
|
||||
|
||||
if (world_id->empty()) {
|
||||
*error_out = "If specified, `worldId` must be non-empty.";
|
||||
return false;
|
||||
// Specifying an empty-string world ID is valid, and will use the default
|
||||
// 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) == '_') {
|
||||
|
@ -106,6 +106,7 @@ void UserScriptWorldConfigurationManager::SetUserScriptWorldInfo(
|
||||
const std::optional<std::string>& world_id,
|
||||
std::optional<std::string> csp,
|
||||
bool enable_messaging) {
|
||||
CHECK(!world_id || !world_id->empty());
|
||||
// Persist world configuratation in ExtensionPrefs.
|
||||
ExtensionPrefs::ScopedDictionaryUpdate update(
|
||||
extension_prefs_, extension.id(), kUserScriptsWorldsConfiguration.name);
|
||||
@ -130,6 +131,7 @@ void UserScriptWorldConfigurationManager::SetUserScriptWorldInfo(
|
||||
void UserScriptWorldConfigurationManager::ClearUserScriptWorldInfo(
|
||||
const Extension& extension,
|
||||
const std::optional<std::string>& world_id) {
|
||||
CHECK(!world_id || !world_id->empty());
|
||||
ExtensionPrefs::ScopedDictionaryUpdate update(
|
||||
extension_prefs_, extension.id(), kUserScriptsWorldsConfiguration.name);
|
||||
std::unique_ptr<prefs::DictionaryValueUpdate> update_dict = update.Get();
|
||||
@ -151,6 +153,7 @@ mojom::UserScriptWorldInfoPtr
|
||||
UserScriptWorldConfigurationManager::GetUserScriptWorldInfo(
|
||||
const ExtensionId& extension_id,
|
||||
const std::optional<std::string>& world_id) {
|
||||
CHECK(!world_id || !world_id->empty());
|
||||
const base::Value::Dict* worlds_configuration =
|
||||
extension_prefs_->ReadPrefAsDictionary(extension_id,
|
||||
kUserScriptsWorldsConfiguration);
|
||||
|
Reference in New Issue
Block a user