tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

browser_extension_sideloading.js (15016B)


      1 /* eslint-disable mozilla/no-arbitrary-setTimeout */
      2 const { AddonManagerPrivate } = ChromeUtils.importESModule(
      3  "resource://gre/modules/AddonManager.sys.mjs"
      4 );
      5 
      6 const { AddonTestUtils } = ChromeUtils.importESModule(
      7  "resource://testing-common/AddonTestUtils.sys.mjs"
      8 );
      9 
     10 AddonTestUtils.initMochitest(this);
     11 
     12 AddonTestUtils.hookAMTelemetryEvents();
     13 
     14 const kSideloaded = true;
     15 
     16 async function createWebExtension(details) {
     17  let options = {
     18    manifest: {
     19      manifest_version: details.manifest_version ?? 2,
     20 
     21      browser_specific_settings: { gecko: { id: details.id } },
     22 
     23      name: details.name,
     24 
     25      permissions: details.permissions,
     26      host_permissions: details.host_permissions,
     27      incognito: details.incognito,
     28    },
     29  };
     30 
     31  if (details.iconURL) {
     32    options.manifest.icons = { 64: details.iconURL };
     33  }
     34 
     35  let xpi = AddonTestUtils.createTempWebExtensionFile(options);
     36 
     37  await AddonTestUtils.manuallyInstall(xpi);
     38 }
     39 
     40 function promiseEvent(eventEmitter, event) {
     41  return new Promise(resolve => {
     42    eventEmitter.once(event, resolve);
     43  });
     44 }
     45 
     46 function getAddonElement(managerWindow, addonId) {
     47  return TestUtils.waitForCondition(
     48    () =>
     49      managerWindow.document.querySelector(`addon-card[addon-id="${addonId}"]`),
     50    `Found entry for sideload extension addon "${addonId}" in HTML about:addons`
     51  );
     52 }
     53 
     54 function assertSideloadedAddonElementState(addonElement, pressed) {
     55  const enableBtn = addonElement.querySelector('[action="toggle-disabled"]');
     56  is(
     57    enableBtn.pressed,
     58    pressed,
     59    `The enable button is ${!pressed ? " not " : ""} pressed`
     60  );
     61  is(enableBtn.localName, "moz-toggle", "The enable button is a toggle");
     62 }
     63 
     64 function clickEnableExtension(addonElement) {
     65  addonElement.querySelector('[action="toggle-disabled"]').click();
     66 }
     67 
     68 add_task(async function test_sideloading() {
     69  const DEFAULT_ICON_URL =
     70    "chrome://mozapps/skin/extensions/extensionGeneric.svg";
     71 
     72  await SpecialPowers.pushPrefEnv({
     73    set: [
     74      ["xpinstall.signatures.required", false],
     75      ["extensions.autoDisableScopes", 15],
     76      ["extensions.ui.disableUnsignedWarnings", true],
     77    ],
     78  });
     79 
     80  Services.fog.testResetFOG();
     81 
     82  const ID1 = "addon1@tests.mozilla.org";
     83  await createWebExtension({
     84    id: ID1,
     85    name: "Test 1",
     86    userDisabled: true,
     87    permissions: ["history", "https://*/*"],
     88    iconURL: "foo-icon.png",
     89  });
     90 
     91  const ID2 = "addon2@tests.mozilla.org";
     92  await createWebExtension({
     93    manifest_version: 3,
     94    id: ID2,
     95    name: "Test 2",
     96    host_permissions: ["<all_urls>"],
     97  });
     98 
     99  const ID3 = "addon3@tests.mozilla.org";
    100  await createWebExtension({
    101    id: ID3,
    102    name: "Test 3",
    103    permissions: ["<all_urls>"],
    104  });
    105 
    106  const ID4 = "addon4@tests.mozilla.org";
    107  await createWebExtension({
    108    id: ID4,
    109    name: "Test 4",
    110    incognito: "not_allowed",
    111    permissions: [],
    112  });
    113 
    114  const ID5 = "addon5@tests.mozilla.org";
    115  await createWebExtension({
    116    id: ID5,
    117    name: "Test 5",
    118    incognito: "not_allowed",
    119    permissions: ["<all_urls>"],
    120  });
    121 
    122  const ID6 = "addon6@tests.mozilla.org";
    123  await createWebExtension({
    124    id: ID6,
    125    name: "Test 6",
    126    incognito: "not_allowed",
    127    permissions: ["history", "https://*/*"],
    128  });
    129 
    130  testCleanup = async function () {
    131    // clear out ExtensionsUI state about sideloaded extensions so
    132    // subsequent tests don't get confused.
    133    ExtensionsUI.sideloaded.clear();
    134    ExtensionsUI.emit("change");
    135  };
    136 
    137  // Navigate away from the starting page to force about:addons to load
    138  // in a new tab during the tests below.
    139  BrowserTestUtils.startLoadingURIString(
    140    gBrowser.selectedBrowser,
    141    "about:robots"
    142  );
    143  await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
    144 
    145  registerCleanupFunction(async function () {
    146    // Return to about:blank when we're done
    147    BrowserTestUtils.startLoadingURIString(
    148      gBrowser.selectedBrowser,
    149      "about:blank"
    150    );
    151    await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, {
    152      wantLoad: "about:blank",
    153    });
    154  });
    155 
    156  let changePromise = new Promise(resolve => {
    157    ExtensionsUI.on("change", function listener() {
    158      ExtensionsUI.off("change", listener);
    159      resolve();
    160    });
    161  });
    162  ExtensionsUI._checkForSideloaded();
    163  await changePromise;
    164 
    165  // Check for the addons badge on the hamburger menu
    166  let menuButton = document.getElementById("PanelUI-menu-button");
    167  is(
    168    menuButton.getAttribute("badge-status"),
    169    "addon-alert",
    170    "Should have addon alert badge"
    171  );
    172 
    173  // Find the menu entries for sideloaded extensions
    174  await gCUITestUtils.openMainMenu();
    175 
    176  let addons = PanelUI.addonNotificationContainer;
    177  is(
    178    addons.children.length,
    179    4,
    180    "Have 4 menu entries for sideloaded extensions"
    181  );
    182 
    183  info(
    184    "Test disabling sideloaded addon 1 using the permission prompt secondary button"
    185  );
    186 
    187  // Click the first sideloaded extension
    188  let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
    189  addons.children[0].click();
    190 
    191  // The click should hide the main menu. This is currently synchronous.
    192  Assert.notEqual(
    193    PanelUI.panel.state,
    194    "open",
    195    "Main menu is closed or closing."
    196  );
    197 
    198  // When we get the permissions prompt, we should be at the extensions
    199  // list in about:addons
    200  let panel = await popupPromise;
    201  is(
    202    gBrowser.currentURI.spec,
    203    "about:addons",
    204    "Foreground tab is at about:addons"
    205  );
    206 
    207  const VIEW = "addons://list/extension";
    208  let win = gBrowser.selectedBrowser.contentWindow;
    209 
    210  await TestUtils.waitForCondition(
    211    () => !win.gViewController.isLoading,
    212    "about:addons view is fully loaded"
    213  );
    214  is(
    215    win.gViewController.currentViewId,
    216    VIEW,
    217    "about:addons is at extensions list"
    218  );
    219 
    220  // Check the contents of the notification, then choose "Cancel"
    221  checkNotification(
    222    panel,
    223    /\/foo-icon\.png$/,
    224    [
    225      ["webext-perms-host-description-all-urls"],
    226      ["webext-perms-description-history"],
    227    ],
    228    kSideloaded
    229  );
    230 
    231  panel.secondaryButton.click();
    232 
    233  let [addon1, addon2, addon3, addon4, addon5, addon6] =
    234    await AddonManager.getAddonsByIDs([ID1, ID2, ID3, ID4, ID5, ID6]);
    235  ok(addon1.seen, "Addon should be marked as seen");
    236  is(addon1.userDisabled, true, "Addon 1 should still be disabled");
    237  is(addon2.userDisabled, true, "Addon 2 should still be disabled");
    238  is(addon3.userDisabled, true, "Addon 3 should still be disabled");
    239  is(addon4.userDisabled, true, "Addon 4 should still be disabled");
    240  is(addon5.userDisabled, true, "Addon 5 should still be disabled");
    241  is(addon6.userDisabled, true, "Addon 6 should still be disabled");
    242 
    243  BrowserTestUtils.removeTab(gBrowser.selectedTab);
    244 
    245  // Should still have 2 entries in the hamburger menu
    246  await gCUITestUtils.openMainMenu();
    247 
    248  addons = PanelUI.addonNotificationContainer;
    249  is(
    250    addons.children.length,
    251    4,
    252    "Have 4 menu entries for sideloaded extensions"
    253  );
    254 
    255  // Close the hamburger menu and go directly to the addons manager
    256  await gCUITestUtils.hideMainMenu();
    257 
    258  win = await BrowserAddonUI.openAddonsMgr(VIEW);
    259  await waitAboutAddonsViewLoaded(win.document);
    260 
    261  // about:addons addon entry element.
    262  const addonElement = await getAddonElement(win, ID2);
    263 
    264  assertSideloadedAddonElementState(addonElement, false);
    265 
    266  info("Test enabling sideloaded addon 2 from about:addons enable button");
    267 
    268  // When clicking enable we should see the permissions notification
    269  popupPromise = promisePopupNotificationShown("addon-webext-permissions");
    270  clickEnableExtension(addonElement);
    271  panel = await popupPromise;
    272  checkNotification(
    273    panel,
    274    DEFAULT_ICON_URL,
    275    [["webext-perms-host-description-all-urls"]],
    276    kSideloaded
    277  );
    278  ok(
    279    panel.querySelector(".webext-perm-privatebrowsing moz-checkbox"),
    280    "Expect incognito checkbox in sideload prompt"
    281  );
    282 
    283  // Accept the permissions
    284  panel.button.click();
    285  await promiseEvent(ExtensionsUI, "change");
    286 
    287  addon2 = await AddonManager.getAddonByID(ID2);
    288  is(addon2.userDisabled, false, "Addon 2 should be enabled");
    289  assertSideloadedAddonElementState(addonElement, true);
    290 
    291  // Should still have 1 entry in the hamburger menu
    292  await gCUITestUtils.openMainMenu();
    293 
    294  addons = PanelUI.addonNotificationContainer;
    295  is(addons.children.length, 4, "Have 4 menu entry for sideloaded extensions");
    296 
    297  // Close the hamburger menu and go to the detail page for this addon
    298  await gCUITestUtils.hideMainMenu();
    299 
    300  win = await BrowserAddonUI.openAddonsMgr(
    301    `addons://detail/${encodeURIComponent(ID3)}`
    302  );
    303 
    304  // Trigger all remaining addon install from the app menu, to be able to cover the
    305  // post install notification that should be triggered when the permission
    306  // dialog is accepted from that flow.
    307  const enableSideloadedFromAppMenu = async (addon, testPanelCb) => {
    308    popupPromise = promisePopupNotificationShown("addon-webext-permissions");
    309    ExtensionsUI.showSideloaded(gBrowser, addon);
    310    panel = await popupPromise;
    311    await testPanelCb();
    312    // Accept the permissions
    313    panel.button.click();
    314    await promiseEvent(ExtensionsUI, "change");
    315  };
    316 
    317  info("Test enabling sideloaded addon 3 from app menu");
    318  await enableSideloadedFromAppMenu(addon3, () => {
    319    checkNotification(
    320      panel,
    321      DEFAULT_ICON_URL,
    322      [["webext-perms-host-description-all-urls"]],
    323      kSideloaded
    324    );
    325  });
    326 
    327  addon3 = await AddonManager.getAddonByID(ID3);
    328  is(addon3.userDisabled, false, "Addon 3 should be enabled");
    329 
    330  addons = PanelUI.addonNotificationContainer;
    331  is(addons.children.length, 3, "Have 3 menu entry for sideloaded extensions");
    332 
    333  info("Test enabling sideloaded addon 4 from app menu");
    334  await enableSideloadedFromAppMenu(addon4, () => {
    335    checkNotification(panel, DEFAULT_ICON_URL, [], kSideloaded);
    336  });
    337  addon4 = await AddonManager.getAddonByID(ID4);
    338  is(addon4.userDisabled, false, "Addon 4 should be enabled");
    339  addons = PanelUI.addonNotificationContainer;
    340  is(addons.children.length, 2, "Have 2 menu entry for sideloaded extensions");
    341 
    342  info("Test enabling sideloaded addon 5 from app menu");
    343  await enableSideloadedFromAppMenu(addon5, () => {
    344    checkNotification(
    345      panel,
    346      DEFAULT_ICON_URL,
    347      [["webext-perms-host-description-all-urls"]],
    348      kSideloaded
    349    );
    350  });
    351  addon5 = await AddonManager.getAddonByID(ID5);
    352  is(addon5.userDisabled, false, "Addon 5 should be enabled");
    353  addons = PanelUI.addonNotificationContainer;
    354  is(addons.children.length, 1, "Have 1 menu entry for sideloaded extensions");
    355 
    356  info("Test enabling sideloaded addon 6 from app menu");
    357  await enableSideloadedFromAppMenu(addon6, () => {
    358    checkNotification(
    359      panel,
    360      DEFAULT_ICON_URL,
    361      [
    362        ["webext-perms-host-description-all-urls"],
    363        ["webext-perms-description-history"],
    364      ],
    365      kSideloaded
    366    );
    367  });
    368  addon6 = await AddonManager.getAddonByID(ID6);
    369  is(addon6.userDisabled, false, "Addon 6 should be enabled");
    370 
    371  isnot(
    372    menuButton.getAttribute("badge-status"),
    373    "addon-alert",
    374    "Should no longer have addon alert badge"
    375  );
    376 
    377  await new Promise(resolve => setTimeout(resolve, 100));
    378 
    379  for (let addon of [addon1, addon2, addon3, addon4, addon5, addon6]) {
    380    await addon.uninstall();
    381  }
    382 
    383  isnot(
    384    menuButton.getAttribute("badge-status"),
    385    "addon-alert",
    386    "Should no longer have addon alert badge"
    387  );
    388 
    389  BrowserTestUtils.removeTab(gBrowser.selectedTab);
    390 
    391  // Assert that the expected AddonManager telemetry are being recorded.
    392  const expectedExtra = { source: "app-profile", method: "sideload" };
    393 
    394  const baseEvent = { object: "extension", extra: expectedExtra };
    395  const createBaseEventAddon = n => ({
    396    ...baseEvent,
    397    value: `addon${n}@tests.mozilla.org`,
    398  });
    399  const getEventsForAddonId = (events, addonId) =>
    400    events.filter(ev => ev.value === addonId);
    401 
    402  const amEvents = AddonTestUtils.getAMTelemetryEvents();
    403 
    404  // Test telemetry events for addon1 (1 permission and 1 origin).
    405  info("Test telemetry events collected for addon1");
    406 
    407  const baseEventAddon1 = createBaseEventAddon(1);
    408 
    409  const blocklist_state = `${Ci.nsIBlocklistService.STATE_NOT_BLOCKED}`;
    410 
    411  Assert.deepEqual(
    412    AddonTestUtils.getAMGleanEvents("manage", { addon_id: ID1 }),
    413    [
    414      {
    415        addon_id: ID1,
    416        method: "sideload_prompt",
    417        addon_type: "extension",
    418        source: "app-profile",
    419        source_method: "sideload",
    420        num_strings: "2",
    421        blocklist_state,
    422      },
    423      {
    424        addon_id: ID1,
    425        method: "uninstall",
    426        addon_type: "extension",
    427        source: "app-profile",
    428        source_method: "sideload",
    429        blocklist_state,
    430      },
    431    ],
    432    "Got the expected Glean events for addon1."
    433  );
    434 
    435  const collectedEventsAddon1 = getEventsForAddonId(
    436    amEvents,
    437    baseEventAddon1.value
    438  );
    439  const expectedEventsAddon1 = [
    440    {
    441      ...baseEventAddon1,
    442      method: "sideload_prompt",
    443      extra: { ...expectedExtra, num_strings: "2", blocklist_state },
    444    },
    445    {
    446      ...baseEventAddon1,
    447      method: "uninstall",
    448      extra: { ...expectedExtra, blocklist_state },
    449    },
    450  ];
    451 
    452  let i = 0;
    453  for (let event of collectedEventsAddon1) {
    454    Assert.deepEqual(
    455      event,
    456      expectedEventsAddon1[i++],
    457      "Got the expected telemetry event"
    458    );
    459  }
    460 
    461  is(
    462    collectedEventsAddon1.length,
    463    expectedEventsAddon1.length,
    464    "Got the expected number of telemetry events for addon1"
    465  );
    466 
    467  const baseEventAddon2 = createBaseEventAddon(2);
    468  const collectedEventsAddon2 = getEventsForAddonId(
    469    amEvents,
    470    baseEventAddon2.value
    471  );
    472  const expectedEventsAddon2 = [
    473    {
    474      ...baseEventAddon2,
    475      method: "sideload_prompt",
    476      extra: { ...expectedExtra, num_strings: "1", blocklist_state },
    477    },
    478    {
    479      ...baseEventAddon2,
    480      method: "enable",
    481      extra: { ...expectedExtra, blocklist_state },
    482    },
    483    {
    484      ...baseEventAddon2,
    485      method: "uninstall",
    486      extra: { ...expectedExtra, blocklist_state },
    487    },
    488  ];
    489 
    490  i = 0;
    491  for (let event of collectedEventsAddon2) {
    492    Assert.deepEqual(
    493      event,
    494      expectedEventsAddon2[i++],
    495      "Got the expected telemetry event"
    496    );
    497  }
    498 
    499  is(
    500    collectedEventsAddon2.length,
    501    expectedEventsAddon2.length,
    502    "Got the expected number of telemetry events for addon2"
    503  );
    504 
    505  Assert.deepEqual(
    506    AddonTestUtils.getAMGleanEvents("manage", { addon_id: ID2 }),
    507    [
    508      {
    509        addon_id: ID2,
    510        method: "sideload_prompt",
    511        addon_type: "extension",
    512        source: "app-profile",
    513        source_method: "sideload",
    514        num_strings: "1",
    515        blocklist_state,
    516      },
    517      {
    518        addon_id: ID2,
    519        method: "enable",
    520        addon_type: "extension",
    521        source: "app-profile",
    522        source_method: "sideload",
    523        blocklist_state,
    524      },
    525      {
    526        addon_id: ID2,
    527        method: "uninstall",
    528        addon_type: "extension",
    529        source: "app-profile",
    530        source_method: "sideload",
    531        blocklist_state,
    532      },
    533    ],
    534    "Got the expected Glean events for addon2."
    535  );
    536 });