chromium/chrome/test/data/extensions/api_test/user_scripts/configure_world/worker.js

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {openTab} from '/_test_resources/test_util/tabs_util.js';

// Error when messaging is not allowed.
const noConnectionError =
    'Error: Could not establish connection. Receiving end does not exist.';

// Script that changes the tab title corresponding to whether eval() is allowed.
const evalScript = `let result;
    try {
      eval('result = "eval allowed"');
      document.title = 'eval allowed';
    } catch (e) {
      document.title = 'eval disallowed';
    }`;

// Script that changes the tab title when messaging is not enabled.
const messagingDisabledScript =
    `let message = (chrome.runtime?.sendMessage === undefined)
         ? 'messaging disabled' : 'messaging present';
       document.title = message;`;

// Script that sends a message, and a second one on response.
const sendMessageScript = `(async function() {
       let response = await chrome.runtime.sendMessage({step: 1});
       let secondMessage = response && response.nextStep
         ? {step: 2} : 'unexpected message';
       chrome.runtime.sendMessage(secondMessage);
       }) ();`;

// Script that listens for a message and sends a response.
const receiveMessageScript = `chrome.runtime.onMessage.addListener(
       (message, sender, sendResponse) => {
         sendResponse(message == 'ping' ? 'pong' : 'Bad message');
       });`;

// Sends a hello message.
const helloScript =
    `if (!!chrome.runtime.sendMessage) { chrome.runtime.sendMessage('hello'); }`

// Navigates to an url requested by the extension and returns the opened tab.
async function navigateToRequestedUrl() {
  const config = await chrome.test.getConfig();
  const url = `http://requested.com:${config.testServer.port}/simple.html`;
  let tab = await openTab(url);
  return tab;
}

async function cleanUpState() {
  // Clear all user script registrations.
  await chrome.userScripts.unregister();

  // Clear all user script world configurations.
  const worldConfigs = await chrome.userScripts.getWorldConfigurations();
  for (const config of worldConfigs) {
    await chrome.userScripts.resetWorldConfiguration(config.worldId);
  }
}

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.`);
    await chrome.test.assertPromiseRejects(
        chrome.userScripts.configureWorld({csp: '', worldId: 'a'.repeat(257)}),
        'Error: World IDs must be at most 256 characters.');
    chrome.test.succeed();
  },

  // Tests that a registered user script in the USER_SCRIPT world cannot send or
  // receive messages when messaging is disabled.
  async function UserScriptWorld_messagingDisabled() {
    // Register a user script in the USER_SCRIPT world with messaging disabled
    // (default).
    const userScripts = [
      {id: 'us1', matches: ['*://*/*'], js: [{code: messagingDisabledScript}]}
    ];
    await chrome.userScripts.register(userScripts);
    await chrome.userScripts.configureWorld({messaging: false});

    const tab = await navigateToRequestedUrl()

    // Verify user script cannot send messages.
    chrome.test.assertEq('messaging disabled', tab.title);

    // Verify user script cannot receive messages.
    await chrome.test.assertPromiseRejects(
        chrome.tabs.sendMessage(tab.id, 'ping'), noConnectionError);

    chrome.test.succeed();
  },

  // Tests that a registered user script in the USER_SCRIPT world can send or
  // receive messages when messaging is enabled.
  async function UserScriptWorld_messagingEnabled() {
    await cleanUpState();

    // Verify user script can send messages.
    chrome.runtime.onUserScriptMessage.addListener(
        async function listener(message, sender, sendResponse) {
          const url = new URL(sender.url);
          chrome.test.assertEq('requested.com', url.hostname);
          chrome.test.assertEq('/simple.html', url.pathname);
          chrome.test.assertEq(0, sender.frameId);
          chrome.test.assertTrue(!!sender.tab);

          if (message.step == 1) {
            sendResponse({nextStep: true});
          } else {
            chrome.test.assertEq(2, message.step);
            chrome.runtime.onUserScriptMessage.removeListener(listener);
            chrome.test.succeed();
          }
        });

    // Register a user script in the USER_SCRIPT world (default) with messaging
    // enabled.
    let userScripts =
        [{id: 'us1', matches: ['*://*/*'], js: [{code: receiveMessageScript}]}];
    await chrome.userScripts.register(userScripts);
    await chrome.userScripts.configureWorld({messaging: true});

    const tab = await navigateToRequestedUrl();

    // Verify user script can receive messages.
    let response = await chrome.tabs.sendMessage(tab.id, 'ping');
    chrome.test.assertEq('pong', response);

    await chrome.userScripts.unregister();

    // Register a user script that sends a message.
    userScripts =
        [{id: 'us2', matches: ['*://*/*'], js: [{code: sendMessageScript}]}];
    chrome.userScripts.register(userScripts);

    await navigateToRequestedUrl();
  },

  // Tests that a registered user script in the MAIN world cannot send or
  // receive messages when messaging is enabled.
  async function mainWorld_messagingAlwaysDisabled() {
    await cleanUpState();

    // Register a user script in the MAIN world.
    const userScripts = [{
      id: 'us1',
      matches: ['*://*/*'],
      js: [{code: messagingDisabledScript}],
      world: chrome.userScripts.ExecutionWorld.MAIN
    }];
    await chrome.userScripts.register(userScripts);

    // Enabling USER_SCRIPT world messaging shouldn't not affect user scripts
    // in the MAIN world, which don't have access to messaging APIs.
    chrome.userScripts.configureWorld({messaging: true});

    const tab = await navigateToRequestedUrl()

    // Verify user script cannot send messages.
    chrome.test.assertEq('messaging disabled', tab.title);

    // Verify user script cannot receive messages.
    await chrome.test.assertPromiseRejects(
        chrome.tabs.sendMessage(tab.id, 'ping'), noConnectionError);

    chrome.test.succeed();
  },

  // Test that enabling messaging affects all registered scripts.
  async function messagingEnabled_MultipleScripts() {
    await cleanUpState();

    // Verify all scripts sent messages.
    let hellocount = 0;
    chrome.runtime.onUserScriptMessage.addListener(
        async function listener(message, sender, sendResponse) {
          if (message == 'hello') {
            hellocount++;
          }
          if (hellocount == 3) {
            chrome.runtime.onUserScriptMessage.removeListener(listener);
            chrome.test.succeed()
          }
        });

    // Register user scripts in the USER_SCRIPT world (default).
    const userScripts = [
      {
        id: 'us1',
        matches: ['*://*/*'],
        js: [{code: helloScript}, {code: helloScript}]
      },
      {id: 'us2', matches: ['*://*/*'], js: [{code: helloScript}]}
    ];
    await chrome.userScripts.register(userScripts);
    await chrome.userScripts.configureWorld({messaging: true});

    // Navigate to a requested page, where the user scripts should send messages
    // back.
    await navigateToRequestedUrl();
  },

  // Tests that configuring the user script world affects scripts registered in
  // the USER_SCRIPT world.
  async function configureCsp_UserScriptWorld() {
    await cleanUpState();

    // Register a user script in the USER_SCRIPT world (default).
    let userScripts =
        [{id: 'us1', matches: ['*://*/*'], js: [{code: evalScript}]}];
    await chrome.userScripts.register(userScripts);

    let tab = await navigateToRequestedUrl()

    // User script defaults to the extension's csp (which disallows eval()).
    chrome.test.assertEq('eval disallowed', tab.title);

    // Update the user script world csp to allow eval().
    await chrome.userScripts.configureWorld({csp: `script-src 'unsafe-eval'`});
    tab = await navigateToRequestedUrl()

    // User script uses the customized csp.
    chrome.test.assertEq('eval allowed', tab.title);

    // Reset the csp. Currently this is only achievable by calling
    // userScripts.configureWorld with no csp value (crbug.com/1497059).
    await chrome.userScripts.configureWorld({});
    tab = await navigateToRequestedUrl()

    // User script eses the extension's csp.
    chrome.test.assertEq('eval disallowed', tab.title);

    chrome.test.succeed();
  },

  // Tests that configuring the user script world does not affect scripts
  // registered in the MAIN world.
  async function configureCsp_MainWorldNotAffected() {
    await cleanUpState();

    // Register a user script in the USER_SCRIPT world (default) with a script
    // that changes the doc title if it can eval() some code.
    let userScripts = [{
      id: 'us1',
      matches: ['*://*/*'],
      js: [{code: evalScript}],
      world: chrome.userScripts.ExecutionWorld.MAIN
    }];
    await chrome.userScripts.register(userScripts);

    // Update the user script to allow eval().
    await chrome.userScripts.configureWorld({csp: `script-src 'unsafe-eval'`});

    // Navigate to a site whose page csp disallows eval().
    const config = await chrome.test.getConfig();
    const url =
        `http://requested.com:${config.testServer.port}/csp-script-src.html`;
    let tab = await openTab(url);

    // User script in the MAIN wold should use the page csp.
    chrome.test.assertEq('eval disallowed', tab.title);

    chrome.test.succeed();
  },

  // Verify that multiple user script worlds are unique and can each have a
  // different CSP specified.
  async function configureWorld_MultipleWorldsAreUnique_Csp() {
    await cleanUpState();

    // Configure "world 1" to allow eval.
    await chrome.userScripts.configureWorld(
        {
          worldId: 'world 1',
          csp: `script-src 'unsafe-eval'`,
        });

    const evalScriptWithWorld =
        `try {
           eval('result = "eval allowed"');
           document.title = document.title + 'WORLD_ID: eval allowed';
         } catch (e) {
           document.title = document.title + 'WORLD_ID: eval disallowed';
         }`;
    const world1Script = evalScriptWithWorld.replaceAll('WORLD_ID', 'world 1');
    const world2Script = evalScriptWithWorld.replaceAll('WORLD_ID', 'world 2');

    // Register two user scripts: one to inject in "world 1" and a second to
    // inject in "world 2".
    const userScripts =
        [
          {
            id: 'us1',
            matches: ['*://*/*'],
            js: [{code: world1Script}],
            worldId: 'world 1',
          },
          {
            id: 'us2',
            matches: ['*://*/*'],
            js: [{code: world2Script}],
            worldId: 'world2',
          },
        ];
    await chrome.userScripts.register(userScripts);

    // Inject the scripts. The first world should allow eval, while the second
    // should not.
    let tab = await navigateToRequestedUrl();
    chrome.test.assertTrue(tab.title.includes('world 1: eval allowed'),
                           tab.title);
    chrome.test.assertTrue(tab.title.includes('world 2: eval disallowed'),
                           tab.title);
    chrome.test.succeed();
  },

  // Verify that multiple user script worlds are unique and can each have a
  // different value for whether messaging APIs are exposed.
  async function configureWorld_MultipleWorldsAreUnique_Messaging() {
    await cleanUpState();

    // Configure "world 1" to allow messaging APIs.
    await chrome.userScripts.configureWorld(
        {
          worldId: 'world 1',
          messaging: true,
        });

    const messagingScriptWithWorld =
        `let titleAddition;
         if (chrome && chrome.runtime && chrome.runtime.sendMessage) {
           titleAddition = 'WORLD_ID: messaging enabled';
         } else {
           titleAddition = 'WORLD_ID: messaging disabled';
         }
         document.title = document.title + titleAddition;`;
    const world1Script =
        messagingScriptWithWorld.replaceAll('WORLD_ID', 'world 1');
    const world2Script =
        messagingScriptWithWorld.replaceAll('WORLD_ID', 'world 2');

    // Register two user scripts: one to inject in "world 1" and a second to
    // inject in "world 2".
    const userScripts =
        [
          {
            id: 'us1',
            matches: ['*://*/*'],
            js: [{code: world1Script}],
            worldId: 'world 1',
          },
          {
            id: 'us2',
            matches: ['*://*/*'],
            js: [{code: world2Script}],
            worldId: 'world2',
          },
        ];
    await chrome.userScripts.register(userScripts);

    // Inject the scripts. The first world should allow messaging, while the
    // second should not.
    let tab = await navigateToRequestedUrl();
    chrome.test.assertTrue(tab.title.includes('world 1: messaging enabled'),
                           tab.title);
    chrome.test.assertTrue(tab.title.includes('world 2: messaging disabled'),
                           tab.title);
    chrome.test.succeed();
  },

  async function canOnlyConfigureUpTo100UserScriptWorlds() {
    await cleanUpState();

    const worldConfigTemplate =
        {
          worldId: 'world #',
          csp: 'test',
        };
    // Register 100 user script world configurations. This should succeed
    // (100 is the current limit).
    for (let i = 0; i < 100; ++i) {
      let worldConfig = { ...worldConfigTemplate };
      worldConfig.worldId += i;
      await chrome.userScripts.configureWorld(worldConfig);
    }

    // Attempting to register one more should fail.
    let worldConfig = { ...worldConfigTemplate };
    worldConfig.worldId += '100';
    await chrome.test.assertPromiseRejects(
        chrome.userScripts.configureWorld(worldConfig),
        'Error: You may only configure up to 100 ' +
            'individual user script worlds.');

    chrome.test.succeed();
  },

  async function canOnlyInjectIn10ActiveWorldsInADocument() {
    await cleanUpState();

    const defaultWorldConfig =
        {
          csp: `script-src 'unsafe-eval'`,
          messaging: true,
        };
    const nonDefaultWorldConfigTemplate =
        {
          csp: `script-src 'self'`,
          messaging: true,
        };
    const injectionTemplate =
        `(() => {
           let result = '<unset>';
           try {
             eval('result = "eval allowed"');
             evalAllowed = 'eval allowed';
           } catch (e) {
             evalAllowed = 'eval disallowed';
           }
           const msg = {worldId: WORLD_ID, result: evalAllowed};
           chrome.runtime.sendMessage('EXT_ID', msg);
         })();`

    // Configure the default world to allow eval.
    await chrome.userScripts.configureWorld(defaultWorldConfig);

    // Returns a zero-padded world ID. Today, scripts are injected in the order
    // defined by their alphabetical ID, rather than by their registration
    // order; this means that a script with ID '10' would inject before ID '2'.
    // We need scripts to inject in a defined order below, so zero-pad the IDs
    // (so it would be 10 vs 02, where 02 injects first).
    // TODO(https://crbug.com/337078958): User scripts should be injected based
    // registration order.
    function getPaddedWorldId(i) {
      return ('' + i).padStart(2, '0');
    }

    // Configure 12 extra worlds, IDs '0' through '11', to not allow eval.
    for (let i = 0; i < 12; ++i) {
      let worldConfig = { ...nonDefaultWorldConfigTemplate };
      worldConfig.worldId = getPaddedWorldId(i);
      await chrome.userScripts.configureWorld(worldConfig);
    }

    // Register 12 user scripts, each injecting into a different world, IDs
    // '0' through '11'.
    let allScripts = [];
    for (let i = 0; i < 12; ++i) {
      const worldId = getPaddedWorldId(i);
      const script = injectionTemplate.replace('WORLD_ID', worldId)
                                      .replace('EXT_ID', chrome.runtime.id);
      const userScript =
          {
            id: 'script-' + worldId,
            matches: ['*://*/*'],
            js: [{code: script}],
            worldId,
          };
      allScripts.push(userScript);
    }
    await chrome.userScripts.register(allScripts);

    let msgCount = 0;
    chrome.runtime.onUserScriptMessage.addListener(
        async function listener(msg) {
          // Each of the first ten scripts (IDs 0 - 9) should have injected into
          // their corresponding world. The 11th and 12th (ID 10 and 11) should
          // have injected into the default world, since we only allow 10 named
          // worlds to be active at a time.
          // We verify which world they injected into by checking whether eval
          // was allowed.
          chrome.test.assertTrue(msg.worldId != undefined);
          chrome.test.assertTrue(msg.worldId < 12);
          const expected =
              msg.worldId < 10 ? 'eval disallowed' : 'eval allowed';
          chrome.test.assertEq(expected, msg.result,
                               'Failed for msg: ' + JSON.stringify(msg));

          ++msgCount;
          if (msgCount == 12) {  // All messages responded.
            chrome.runtime.onUserScriptMessage.removeListener(listener);
            chrome.test.succeed();
          }
        });

    await navigateToRequestedUrl();
  },
]);